dars-framework 1.0.6__tar.gz → 1.0.7__tar.gz
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.
- {dars_framework-1.0.6/dars_framework.egg-info → dars_framework-1.0.7}/PKG-INFO +6 -4
- {dars_framework-1.0.6 → dars_framework-1.0.7}/README.md +4 -2
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/core/app.py +319 -48
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/exporters/base.py +13 -4
- dars_framework-1.0.6/dars/exporters/web/html_css_js.py → dars_framework-1.0.7/dars/exporters/web/OLD/html_css_js_OLD4.py +109 -127
- dars_framework-1.0.7/dars/exporters/web/html_css_js.py +1671 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/templates/examples/advanced/advanced_modal_demo.py +2 -2
- dars_framework-1.0.7/dars/templates/examples/demo/complete_app.py +24 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/version.py +2 -2
- {dars_framework-1.0.6 → dars_framework-1.0.7/dars_framework.egg-info}/PKG-INFO +6 -4
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars_framework.egg-info/SOURCES.txt +1 -0
- dars_framework-1.0.7/dars_framework.egg-info/requires.txt +4 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/pyproject.toml +2 -4
- dars_framework-1.0.6/dars/templates/examples/demo/complete_app.py +0 -720
- dars_framework-1.0.6/dars_framework.egg-info/requires.txt +0 -4
- {dars_framework-1.0.6 → dars_framework-1.0.7}/LICENSE +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/__init__.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/all.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/cli/__init__.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/cli/hot_reload.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/cli/main.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/cli/preview.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/cli/translations.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/__init__.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/advanced/__init__.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/advanced/accordion.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/advanced/card.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/advanced/modal.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/advanced/navbar.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/advanced/table.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/advanced/tabs.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/basic/__init__.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/basic/button.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/basic/checkbox.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/basic/container.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/basic/datepicker.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/basic/image.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/basic/input.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/basic/link.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/basic/page.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/basic/progressbar.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/basic/radiobutton.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/basic/select.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/basic/slider.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/basic/spinner.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/basic/text.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/basic/textarea.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/basic/tooltip.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/layout/__init__.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/layout/anchor.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/layout/flex.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/layout/grid.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/core/__init__.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/core/component.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/core/events.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/core/properties.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/docs/__init__.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/exporters/__init__.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/exporters/web/OLD/html_css_js_old.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/exporters/web/OLD/html_css_js_old2.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/exporters/web/__init__.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/scripts/__init__.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/scripts/dscript.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/scripts/script.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/templates/__init__.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/templates/examples/advanced/all_components_demo.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/templates/examples/advanced/dashboard.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/templates/examples/advanced/modern_web_app.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/templates/examples/basic/flex_layout_responsive.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/templates/examples/basic/form_components.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/templates/examples/basic/grid_layout_responsive.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/templates/examples/basic/hello_world.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/templates/examples/basic/layout_multipage_demo.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/templates/examples/basic/multipage_example.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/templates/examples/basic/pwa_custom_icons.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/templates/examples/basic/simple_form.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/templates/html/__init__.py +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars_framework.egg-info/dependency_links.txt +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars_framework.egg-info/entry_points.txt +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/dars_framework.egg-info/top_level.txt +0 -0
- {dars_framework-1.0.6 → dars_framework-1.0.7}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dars-framework
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.7
|
|
4
4
|
Summary: Dars is a Python UI framework for building modern, interactive web apps with only Python code. Write your interface in Python, export it to static HTML/CSS/JS, and deploy anywhere.
|
|
5
5
|
Author-email: ztamdev <zondax2009@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -29,13 +29,15 @@ Description-Content-Type: text/markdown
|
|
|
29
29
|
License-File: LICENSE
|
|
30
30
|
Requires-Dist: rich
|
|
31
31
|
Requires-Dist: bs4
|
|
32
|
+
Requires-Dist: uvicorn
|
|
32
33
|
Requires-Dist: fastapi
|
|
33
|
-
Requires-Dist: uvicorn[standard]
|
|
34
34
|
Dynamic: license-file
|
|
35
35
|
|
|
36
|
-
# Dars
|
|
36
|
+
# Dars Framework
|
|
37
37
|
|
|
38
|
-
Dars
|
|
38
|
+
Dars Landing Page: https://ztamdev.github.io/Dars-Framework/
|
|
39
|
+
|
|
40
|
+
Dars is a Python UI framework for building modern, interactive web apps with Python code. Write your interface in Python, export it to static HTML/CSS/JS, and deploy anywhere.
|
|
39
41
|
|
|
40
42
|
> Some Javascript or frontend stack required.
|
|
41
43
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
# Dars
|
|
1
|
+
# Dars Framework
|
|
2
2
|
|
|
3
|
-
Dars
|
|
3
|
+
Dars Landing Page: https://ztamdev.github.io/Dars-Framework/
|
|
4
|
+
|
|
5
|
+
Dars is a Python UI framework for building modern, interactive web apps with Python code. Write your interface in Python, export it to static HTML/CSS/JS, and deploy anywhere.
|
|
4
6
|
|
|
5
7
|
> Some Javascript or frontend stack required.
|
|
6
8
|
|
|
@@ -4,12 +4,13 @@ from .events import EventManager
|
|
|
4
4
|
|
|
5
5
|
class Page:
|
|
6
6
|
"""Representa una página individual en la app Dars (multipágina)."""
|
|
7
|
-
def __init__(self, name: str, root: 'Component', title: str = None, meta: dict = None, index: bool = False):
|
|
7
|
+
def __init__(self, name: str, root: 'Component', title: str = None, meta: dict = None, index: bool = False, scripts: Optional[List[Any]] = None):
|
|
8
8
|
self.name = name # slug o nombre de la página
|
|
9
9
|
self.root = root # componente raíz de la página
|
|
10
10
|
self.title = title
|
|
11
11
|
self.meta = meta or {}
|
|
12
12
|
self.index = index # ¿Es la página principal?
|
|
13
|
+
self.scripts: List[Any] = list(scripts) if scripts else []
|
|
13
14
|
|
|
14
15
|
def attr(self, **attrs):
|
|
15
16
|
"""
|
|
@@ -29,12 +30,110 @@ class Page:
|
|
|
29
30
|
d['root'] = self.root
|
|
30
31
|
d['title'] = self.title
|
|
31
32
|
d['index'] = self.index
|
|
33
|
+
d['scripts'] = list(self.scripts)
|
|
32
34
|
return d
|
|
35
|
+
# -----------------------------
|
|
36
|
+
# Métodos para manejar scripts
|
|
37
|
+
# -----------------------------
|
|
38
|
+
def add_script(self, script: Any):
|
|
39
|
+
"""
|
|
40
|
+
Agrega un script a esta página.
|
|
41
|
+
- Si 'script' es una instancia (p. ej. InlineScript/FileScript/DScript), se añade tal cual.
|
|
42
|
+
- Si 'script' es una cadena, se interpreta como InlineScript (código).
|
|
43
|
+
- Si 'script' es un dict, se añade tal cual (fallback).
|
|
44
|
+
Devuelve self para encadenar llamadas.
|
|
45
|
+
"""
|
|
46
|
+
# si es str => interpretarlo como inline
|
|
47
|
+
if isinstance(script, str):
|
|
48
|
+
created = self._make_inline_script(script)
|
|
49
|
+
self.scripts.append(created)
|
|
50
|
+
return self
|
|
51
|
+
|
|
52
|
+
# si es dict => fallback, guardarlo
|
|
53
|
+
if isinstance(script, dict):
|
|
54
|
+
self.scripts.append(script)
|
|
55
|
+
return self
|
|
56
|
+
|
|
57
|
+
# si ya es una instancia de "Script" (no podemos verificar tipo concreto sin dependencia),
|
|
58
|
+
# asumimos que es un script válido y lo añadimos.
|
|
59
|
+
self.scripts.append(script)
|
|
60
|
+
return self
|
|
61
|
+
|
|
62
|
+
# alias corto (pedido)
|
|
63
|
+
def addscript(self, script: Any):
|
|
64
|
+
return self.add_script(script)
|
|
65
|
+
|
|
66
|
+
def add_inline_script(self, code: str, **kwargs):
|
|
67
|
+
"""Convenience: añade un InlineScript a la página (code = JS o similar)."""
|
|
68
|
+
s = self._make_inline_script(code, **kwargs)
|
|
69
|
+
self.scripts.append(s)
|
|
70
|
+
return self
|
|
71
|
+
|
|
72
|
+
def add_file_script(self, path: str, **kwargs):
|
|
73
|
+
"""Convenience: añade un FileScript (referencia a archivo .js/.ts/etc.)"""
|
|
74
|
+
s = self._make_file_script(path, **kwargs)
|
|
75
|
+
self.scripts.append(s)
|
|
76
|
+
return self
|
|
77
|
+
|
|
78
|
+
def add_dscript(self, obj: Any, **kwargs):
|
|
79
|
+
"""Convenience: intenta crear/añadir un DScript (si existe la clase)."""
|
|
80
|
+
s = self._make_dscript(obj, **kwargs)
|
|
81
|
+
self.scripts.append(s)
|
|
82
|
+
return self
|
|
83
|
+
|
|
84
|
+
def get_scripts(self) -> List[Any]:
|
|
85
|
+
"""Retorna la lista de scripts añadidos a la página."""
|
|
86
|
+
return list(self.scripts)
|
|
87
|
+
|
|
88
|
+
# -----------------------------
|
|
89
|
+
# Helpers para construcción segura
|
|
90
|
+
# -----------------------------
|
|
91
|
+
def _make_inline_script(self, code: str, **kwargs) -> Any:
|
|
92
|
+
"""
|
|
93
|
+
Intenta crear una instancia InlineScript si existe en dars.scripts.*.
|
|
94
|
+
Si no, devuelve un dict fallback: {'type':'inline','code':..., **kwargs}
|
|
95
|
+
"""
|
|
96
|
+
try:
|
|
97
|
+
# intentamos import común (ajusta según tu layout de módulos si hace falta)
|
|
98
|
+
from dars.scripts import InlineScript # type: ignore
|
|
99
|
+
return InlineScript(code, **kwargs)
|
|
100
|
+
except Exception:
|
|
101
|
+
try:
|
|
102
|
+
from dars.scripts.inline import InlineScript # type: ignore
|
|
103
|
+
return InlineScript(code, **kwargs)
|
|
104
|
+
except Exception:
|
|
105
|
+
# fallback: dict simple que contiene lo mínimo
|
|
106
|
+
return {'type': 'inline', 'code': code, **kwargs}
|
|
107
|
+
|
|
108
|
+
def _make_file_script(self, path: str, **kwargs) -> Any:
|
|
109
|
+
"""
|
|
110
|
+
Intenta crear una instancia FileScript si existe. Si no, devuelve dict fallback.
|
|
111
|
+
"""
|
|
112
|
+
try:
|
|
113
|
+
from dars.scripts import FileScript # type: ignore
|
|
114
|
+
return FileScript(path, **kwargs)
|
|
115
|
+
except Exception:
|
|
116
|
+
try:
|
|
117
|
+
from dars.scripts.file import FileScript # type: ignore
|
|
118
|
+
return FileScript(path, **kwargs)
|
|
119
|
+
except Exception:
|
|
120
|
+
return {'type': 'file', 'path': path, **kwargs}
|
|
121
|
+
|
|
122
|
+
def _make_dscript(self, obj: Any, **kwargs) -> Any:
|
|
123
|
+
"""
|
|
124
|
+
Intenta crear una instancia DScript si existe. Si no, guarda el objeto con marca.
|
|
125
|
+
"""
|
|
126
|
+
try:
|
|
127
|
+
from dars.scripts import DScript # type: ignore
|
|
128
|
+
return DScript(obj, **kwargs)
|
|
129
|
+
except Exception:
|
|
130
|
+
# si ya es dict o similar, solo anotamos el tipo
|
|
131
|
+
return {'type': 'dscript', 'value': obj, **kwargs}
|
|
33
132
|
|
|
34
133
|
class App:
|
|
35
134
|
"""Clase principal que representa una aplicación Dars"""
|
|
36
135
|
|
|
37
|
-
def rTimeCompile(self, exporter=None, port=None):
|
|
136
|
+
def rTimeCompile(self, exporter=None, port=None, add_file_types=None):
|
|
38
137
|
"""
|
|
39
138
|
Genera una preview rápida de la app en un servidor local usando un exportador
|
|
40
139
|
(por defecto HTMLCSSJSExporter) y sirviendo los archivos en un directorio temporal.
|
|
@@ -80,7 +179,40 @@ class App:
|
|
|
80
179
|
port = int(sys.argv[i + 1])
|
|
81
180
|
except Exception:
|
|
82
181
|
pass
|
|
83
|
-
|
|
182
|
+
# --- Normalizar add_file_types => lista de extensiones que empiezan con '.' ---
|
|
183
|
+
def _normalize_exts(exts):
|
|
184
|
+
if not exts:
|
|
185
|
+
return ['.py']
|
|
186
|
+
# aceptar string con comas
|
|
187
|
+
if isinstance(exts, str):
|
|
188
|
+
parts = [p.strip() for p in exts.split(',') if p.strip()]
|
|
189
|
+
elif isinstance(exts, (list, tuple, set)):
|
|
190
|
+
parts = [str(p).strip() for p in exts if p]
|
|
191
|
+
else:
|
|
192
|
+
parts = [str(exts).strip()]
|
|
193
|
+
|
|
194
|
+
normalized = []
|
|
195
|
+
for p in parts:
|
|
196
|
+
if not p:
|
|
197
|
+
continue
|
|
198
|
+
if not p.startswith('.'):
|
|
199
|
+
p = '.' + p
|
|
200
|
+
normalized.append(p.lower())
|
|
201
|
+
# siempre incluir .py (comportamiento: .py + los adicionales)
|
|
202
|
+
if '.py' not in normalized:
|
|
203
|
+
normalized.insert(0, '.py')
|
|
204
|
+
# eliminar duplicados preservando orden
|
|
205
|
+
seen = set()
|
|
206
|
+
result = []
|
|
207
|
+
for e in normalized:
|
|
208
|
+
if e not in seen:
|
|
209
|
+
seen.add(e)
|
|
210
|
+
result.append(e)
|
|
211
|
+
return result
|
|
212
|
+
|
|
213
|
+
# Lista final de extensiones a vigilar (ej: ['.py', '.js', '.css'])
|
|
214
|
+
watch_exts = _normalize_exts(add_file_types)
|
|
215
|
+
|
|
84
216
|
# Importar exportador por defecto si no se pasa
|
|
85
217
|
if exporter is None:
|
|
86
218
|
try:
|
|
@@ -98,8 +230,15 @@ class App:
|
|
|
98
230
|
return
|
|
99
231
|
|
|
100
232
|
shutdown_event = threading.Event()
|
|
233
|
+
watchers = [] # aquí guardaremos todos los watchers
|
|
234
|
+
|
|
235
|
+
# Debounce / lock para evitar reloads concurrentes
|
|
236
|
+
reload_lock = threading.Lock()
|
|
237
|
+
last_reload_at = 0.0
|
|
238
|
+
MIN_RELOAD_INTERVAL = 0.4 # segundos
|
|
239
|
+
|
|
101
240
|
try:
|
|
102
|
-
# Detectar archivo principal de la app
|
|
241
|
+
# Detectar archivo principal de la app (el que ejecutaste con `python archivo.py`)
|
|
103
242
|
app_file = None
|
|
104
243
|
for frame in inspect.stack():
|
|
105
244
|
if frame.function == "<module>":
|
|
@@ -125,7 +264,7 @@ class App:
|
|
|
125
264
|
|
|
126
265
|
os.makedirs(preview_dir, exist_ok=True)
|
|
127
266
|
|
|
128
|
-
# export inicial desde el root
|
|
267
|
+
# export inicial desde el root usando la instancia actual (self)
|
|
129
268
|
with pushd(project_root):
|
|
130
269
|
exporter.export(self, preview_dir)
|
|
131
270
|
|
|
@@ -150,55 +289,178 @@ class App:
|
|
|
150
289
|
# --- HOT RELOAD ---
|
|
151
290
|
from dars.cli.hot_reload import FileWatcher
|
|
152
291
|
|
|
153
|
-
def
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
spec = importlib.util.spec_from_file_location("dars_app", app_file)
|
|
166
|
-
module = importlib.util.module_from_spec(spec)
|
|
167
|
-
spec.loader.exec_module(module)
|
|
168
|
-
|
|
169
|
-
# Buscar instancia App
|
|
170
|
-
new_app = None
|
|
171
|
-
for v in vars(module).values():
|
|
172
|
-
if isinstance(v, App):
|
|
173
|
-
new_app = v
|
|
292
|
+
def _collect_project_files_by_ext(root, exts):
|
|
293
|
+
files = []
|
|
294
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
295
|
+
# excluir preview_dir, .git y __pycache__
|
|
296
|
+
if os.path.abspath(dirpath).startswith(os.path.abspath(preview_dir)):
|
|
297
|
+
continue
|
|
298
|
+
if '.git' in dirpath or '__pycache__' in dirpath:
|
|
299
|
+
continue
|
|
300
|
+
for fname in filenames:
|
|
301
|
+
for ext in exts:
|
|
302
|
+
if fname.lower().endswith(ext):
|
|
303
|
+
files.append(os.path.join(dirpath, fname))
|
|
174
304
|
break
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
305
|
+
return files
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def reload_and_export(changed_file=None):
|
|
309
|
+
nonlocal last_reload_at
|
|
310
|
+
now = time.time()
|
|
311
|
+
# debounce rápido
|
|
312
|
+
if now - last_reload_at < MIN_RELOAD_INTERVAL:
|
|
313
|
+
return
|
|
314
|
+
with reload_lock:
|
|
315
|
+
last_reload_at = time.time()
|
|
316
|
+
if console:
|
|
317
|
+
console.print(f"[yellow]Detected change in {changed_file}. Reloading...[/yellow]")
|
|
318
|
+
else:
|
|
319
|
+
print(f"[Dars] Detected change in {changed_file}. Reloading...")
|
|
320
|
+
|
|
321
|
+
try:
|
|
322
|
+
if project_root not in sys.path:
|
|
323
|
+
sys.path.insert(0, project_root)
|
|
324
|
+
|
|
325
|
+
with pushd(project_root):
|
|
326
|
+
# --- Limpiar del cache todos los módulos que pertenecen al proyecto ---
|
|
327
|
+
to_remove = []
|
|
328
|
+
for name, mod in list(sys.modules.items()):
|
|
329
|
+
try:
|
|
330
|
+
mod_file = getattr(mod, '__file__', None)
|
|
331
|
+
if not mod_file:
|
|
332
|
+
continue
|
|
333
|
+
# normalizar paths
|
|
334
|
+
mod_file_abs = os.path.abspath(mod_file)
|
|
335
|
+
if mod_file_abs.startswith(os.path.abspath(project_root)):
|
|
336
|
+
to_remove.append(name)
|
|
337
|
+
except Exception:
|
|
338
|
+
continue
|
|
339
|
+
|
|
340
|
+
for name in to_remove:
|
|
341
|
+
try:
|
|
342
|
+
del sys.modules[name]
|
|
343
|
+
except Exception:
|
|
344
|
+
pass
|
|
345
|
+
|
|
346
|
+
# también borrar cualquier nombre temporal 'dars_app' si existiese
|
|
347
|
+
sys.modules.pop("dars_app", None)
|
|
348
|
+
|
|
349
|
+
# Importar el archivo principal en un nombre único (para limpieza segura)
|
|
350
|
+
unique_name = f"dars_app_reload_{int(time.time()*1000)}"
|
|
351
|
+
spec = importlib.util.spec_from_file_location(unique_name, app_file)
|
|
352
|
+
module = importlib.util.module_from_spec(spec)
|
|
353
|
+
spec.loader.exec_module(module)
|
|
354
|
+
|
|
355
|
+
# Buscar nueva instancia App en el módulo recargado
|
|
356
|
+
new_app = None
|
|
357
|
+
for v in vars(module).values():
|
|
358
|
+
try:
|
|
359
|
+
if isinstance(v, App):
|
|
360
|
+
new_app = v
|
|
361
|
+
break
|
|
362
|
+
except Exception:
|
|
363
|
+
# si isinstance falla por alguna razón, ignorar
|
|
364
|
+
pass
|
|
365
|
+
|
|
366
|
+
# fallback por nombre de clase (por si App es distinto objeto)
|
|
367
|
+
if not new_app:
|
|
368
|
+
for v in vars(module).values():
|
|
369
|
+
try:
|
|
370
|
+
if hasattr(v, '__class__') and v.__class__.__name__ == 'App':
|
|
371
|
+
new_app = v
|
|
372
|
+
break
|
|
373
|
+
except Exception:
|
|
374
|
+
pass
|
|
375
|
+
|
|
376
|
+
if not new_app:
|
|
377
|
+
(console.print("[red]No App instance found after reload.[/red]")
|
|
378
|
+
if console else print("[Dars] No App instance found after reload."))
|
|
379
|
+
return
|
|
380
|
+
|
|
381
|
+
# Exportar la nueva instancia
|
|
382
|
+
exporter.export(new_app, preview_dir)
|
|
383
|
+
|
|
384
|
+
(console.print("[green]App reloaded and re-exported successfully.[/green]")
|
|
385
|
+
if console else print("[Dars] App reloaded and re-exported successfully."))
|
|
386
|
+
|
|
387
|
+
except Exception as e:
|
|
388
|
+
tb = traceback.format_exc()
|
|
389
|
+
(console.print(f"[red]Hot reload failed: {e}\n{tb}[/red]")
|
|
390
|
+
if console else print(f"[Dars] Hot reload failed: {e}\n{tb}"))
|
|
391
|
+
|
|
392
|
+
# --- Crear watchers para todos los archivos .py dentro del proyecto (recursivo) ---
|
|
393
|
+
files_to_watch = _collect_project_files_by_ext(project_root, watch_exts)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
# Si no hay archivos detectados (raro), al menos mirar app_file
|
|
397
|
+
if not files_to_watch:
|
|
398
|
+
files_to_watch = [app_file]
|
|
399
|
+
|
|
400
|
+
for f in files_to_watch:
|
|
401
|
+
try:
|
|
402
|
+
# FileWatcher espera una función sin argumentos; usamos lambda que captura f
|
|
403
|
+
w = FileWatcher(f, lambda f=f: reload_and_export(f))
|
|
404
|
+
w.start()
|
|
405
|
+
watchers.append(w)
|
|
186
406
|
except Exception as e:
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
407
|
+
if console:
|
|
408
|
+
console.print(f"[yellow]Warning: could not watch {f}: {e}[/yellow]")
|
|
409
|
+
else:
|
|
410
|
+
print(f"[Dars] Warning: could not watch {f}: {e}")
|
|
411
|
+
|
|
412
|
+
if console:
|
|
413
|
+
# Mostrar rutas relativas para que no sea tan largo
|
|
414
|
+
rel_paths = [os.path.relpath(f, project_root) for f in files_to_watch]
|
|
415
|
+
max_show = 80 # número máximo de líneas a mostrar
|
|
416
|
+
if len(rel_paths) > max_show:
|
|
417
|
+
shown = rel_paths[:max_show]
|
|
418
|
+
shown.append(f"... (+{len(rel_paths)-max_show} más)")
|
|
419
|
+
else:
|
|
420
|
+
shown = rel_paths or ["(ninguno)"]
|
|
421
|
+
|
|
422
|
+
from rich.table import Table
|
|
423
|
+
table = Table(show_header=False, box=None, padding=0)
|
|
424
|
+
table.add_column("Files", style="bold")
|
|
425
|
+
for p in shown:
|
|
426
|
+
table.add_row(p)
|
|
427
|
+
|
|
428
|
+
panel = Panel(
|
|
429
|
+
table,
|
|
430
|
+
title=f"Watching {len(files_to_watch)} files · Exts: {', '.join(watch_exts)}",
|
|
431
|
+
subtitle=f"Project root: {os.path.basename(project_root)}",
|
|
432
|
+
border_style="magenta"
|
|
433
|
+
)
|
|
434
|
+
console.print(panel)
|
|
435
|
+
else:
|
|
436
|
+
print(f"[Dars] Watching {len(files_to_watch)} files in {project_root}:")
|
|
437
|
+
for f in files_to_watch:
|
|
438
|
+
print(" -", os.path.relpath(f, project_root))
|
|
192
439
|
|
|
440
|
+
# Loop principal: espera a Ctrl+C
|
|
193
441
|
while not shutdown_event.is_set():
|
|
194
|
-
shutdown_event.wait(timeout=1) # Espera
|
|
442
|
+
shutdown_event.wait(timeout=1) # Espera sin consumir CPU
|
|
443
|
+
|
|
195
444
|
except KeyboardInterrupt:
|
|
196
445
|
shutdown_event.set()
|
|
197
|
-
|
|
446
|
+
for w in watchers:
|
|
447
|
+
try:
|
|
448
|
+
w.stop()
|
|
449
|
+
except Exception:
|
|
450
|
+
pass
|
|
198
451
|
(console.print("\n[cyan]Stopping preview and watcher...[/cyan]")
|
|
199
452
|
if console else print("\n[Dars] Stopping preview and watcher..."))
|
|
200
453
|
finally:
|
|
201
|
-
|
|
454
|
+
# Detener watchers y servidor
|
|
455
|
+
try:
|
|
456
|
+
server.stop()
|
|
457
|
+
except Exception:
|
|
458
|
+
pass
|
|
459
|
+
for w in watchers:
|
|
460
|
+
try:
|
|
461
|
+
w.stop()
|
|
462
|
+
except Exception:
|
|
463
|
+
pass
|
|
202
464
|
(console.print("[green]Preview stopped.[/green]")
|
|
203
465
|
if console else print("[Dars] Preview stopped."))
|
|
204
466
|
|
|
@@ -209,7 +471,11 @@ class App:
|
|
|
209
471
|
msg = f"Unexpected error in fast preview: {e}\n{traceback.format_exc()}"
|
|
210
472
|
console.print(f"[red]{msg}[/red]") if console else print(msg)
|
|
211
473
|
finally:
|
|
212
|
-
|
|
474
|
+
# Restaurar cwd y limpiar preview
|
|
475
|
+
try:
|
|
476
|
+
os.chdir(cwd_original)
|
|
477
|
+
except Exception:
|
|
478
|
+
pass
|
|
213
479
|
try:
|
|
214
480
|
shutil.rmtree(preview_dir)
|
|
215
481
|
(console.print("[yellow]Preview files deleted.[/yellow]")
|
|
@@ -218,7 +484,6 @@ class App:
|
|
|
218
484
|
msg = f"Could not delete preview directory: {e}"
|
|
219
485
|
console.print(f"[red]{msg}[/red]") if console else print(msg)
|
|
220
486
|
|
|
221
|
-
|
|
222
487
|
|
|
223
488
|
def __init__(
|
|
224
489
|
self,
|
|
@@ -539,7 +804,13 @@ class App:
|
|
|
539
804
|
errors.extend(self._validate_component(child, child_path))
|
|
540
805
|
|
|
541
806
|
return errors
|
|
542
|
-
|
|
807
|
+
|
|
808
|
+
def _count_components(self, component: Component) -> int:
|
|
809
|
+
"""Cuenta el número total de componentes en la app (single-page y multipage)"""
|
|
810
|
+
count = 1
|
|
811
|
+
for child in component.children:
|
|
812
|
+
count += self._count_components(child)
|
|
813
|
+
return count
|
|
543
814
|
def get_component_tree(self) -> str:
|
|
544
815
|
"""
|
|
545
816
|
Devuelve una representación legible (string) del árbol de componentes.
|
|
@@ -61,9 +61,18 @@ class Exporter(ABC):
|
|
|
61
61
|
|
|
62
62
|
return "; ".join(css_rules)
|
|
63
63
|
|
|
64
|
-
def generate_unique_id(self, component: 'Component') -> str:
|
|
65
|
-
"""Genera un ID único para un componente si no tiene uno"""
|
|
66
|
-
|
|
64
|
+
def generate_unique_id(self, component: 'Component', prefix: str = "component") -> str:
|
|
65
|
+
"""Genera un ID único para un componente si no tiene uno definido."""
|
|
66
|
+
# Si el usuario ya puso un id, usarlo siempre
|
|
67
|
+
if getattr(component, "id", None):
|
|
67
68
|
return component.id
|
|
68
|
-
|
|
69
|
+
|
|
70
|
+
# Si no tiene id, generar uno único pero persistente
|
|
71
|
+
unique = f"{prefix}_{hex(id(component))}"
|
|
72
|
+
try:
|
|
73
|
+
component.id = unique # lo guardamos en el componente
|
|
74
|
+
except Exception:
|
|
75
|
+
pass # por si el objeto no permite asignación
|
|
76
|
+
return unique
|
|
77
|
+
|
|
69
78
|
|