dars-framework 1.2.3__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.
Files changed (118) hide show
  1. dars/__init__.py +0 -0
  2. dars/all.py +69 -0
  3. dars/cli/__init__.py +0 -0
  4. dars/cli/doctor/__init__.py +1 -0
  5. dars/cli/doctor/detect.py +154 -0
  6. dars/cli/doctor/doctor.py +176 -0
  7. dars/cli/doctor/installers.py +100 -0
  8. dars/cli/doctor/persist.py +62 -0
  9. dars/cli/doctor/preflight.py +33 -0
  10. dars/cli/doctor/ui.py +54 -0
  11. dars/cli/hot_reload.py +33 -0
  12. dars/cli/main.py +1107 -0
  13. dars/cli/preview.py +448 -0
  14. dars/cli/translations.py +531 -0
  15. dars/components/__init__.py +0 -0
  16. dars/components/advanced/__init__.py +8 -0
  17. dars/components/advanced/accordion.py +26 -0
  18. dars/components/advanced/card.py +33 -0
  19. dars/components/advanced/modal.py +45 -0
  20. dars/components/advanced/navbar.py +44 -0
  21. dars/components/advanced/table.py +25 -0
  22. dars/components/advanced/tabs.py +31 -0
  23. dars/components/basic/__init__.py +34 -0
  24. dars/components/basic/button.py +55 -0
  25. dars/components/basic/checkbox.py +35 -0
  26. dars/components/basic/container.py +29 -0
  27. dars/components/basic/datepicker.py +139 -0
  28. dars/components/basic/image.py +36 -0
  29. dars/components/basic/input.py +57 -0
  30. dars/components/basic/link.py +31 -0
  31. dars/components/basic/markdown.py +86 -0
  32. dars/components/basic/page.py +20 -0
  33. dars/components/basic/progressbar.py +18 -0
  34. dars/components/basic/radiobutton.py +35 -0
  35. dars/components/basic/select.py +82 -0
  36. dars/components/basic/slider.py +63 -0
  37. dars/components/basic/spinner.py +12 -0
  38. dars/components/basic/text.py +23 -0
  39. dars/components/basic/textarea.py +46 -0
  40. dars/components/basic/tooltip.py +19 -0
  41. dars/components/layout/__init__.py +0 -0
  42. dars/components/layout/anchor.py +13 -0
  43. dars/components/layout/flex.py +26 -0
  44. dars/components/layout/grid.py +45 -0
  45. dars/config.py +134 -0
  46. dars/core/__init__.py +0 -0
  47. dars/core/app.py +957 -0
  48. dars/core/component.py +284 -0
  49. dars/core/events.py +102 -0
  50. dars/core/js_bridge.py +99 -0
  51. dars/core/properties.py +127 -0
  52. dars/core/state.py +309 -0
  53. dars/dars_tests/apps_test/health_check.py +56 -0
  54. dars/dars_tests/run_tests.py +275 -0
  55. dars/dars_tests/tests/test_advanced_components.py +69 -0
  56. dars/dars_tests/tests/test_basic_components.py +88 -0
  57. dars/dars_tests/tests/test_core_and_cli.py +17 -0
  58. dars/dars_tests/tests/test_layout_components.py +58 -0
  59. dars/dars_tests/tests/test_version_check.py +21 -0
  60. dars/docs/__init__.py +0 -0
  61. dars/docs/app.md +290 -0
  62. dars/docs/cli.md +80 -0
  63. dars/docs/components.md +1679 -0
  64. dars/docs/custom_components.md +30 -0
  65. dars/docs/events.md +45 -0
  66. dars/docs/exporters.md +162 -0
  67. dars/docs/getting_started.md +79 -0
  68. dars/docs/index.md +18 -0
  69. dars/docs/scripts.md +593 -0
  70. dars/docs/state_management.md +57 -0
  71. dars/exporters/__init__.py +0 -0
  72. dars/exporters/base.py +96 -0
  73. dars/exporters/web/OLD/html_css_js_OLD4.py +1538 -0
  74. dars/exporters/web/OLD/html_css_js_old.py +1406 -0
  75. dars/exporters/web/OLD/html_css_js_old2.py +1406 -0
  76. dars/exporters/web/__init__.py +0 -0
  77. dars/exporters/web/html_css_js.py +2675 -0
  78. dars/exporters/web/vdom.py +251 -0
  79. dars/js_lib.py +206 -0
  80. dars/scripts/__init__.py +0 -0
  81. dars/scripts/dscript.py +26 -0
  82. dars/scripts/script.py +39 -0
  83. dars/security.py +195 -0
  84. dars/templates/__init__.py +0 -0
  85. dars/templates/__pycache__/__init__.cpython-311.pyc +0 -0
  86. dars/templates/examples/README.md +4 -0
  87. dars/templates/examples/__pycache__/dynamic_event_demo.cpython-311.pyc +0 -0
  88. dars/templates/examples/advanced/Modal_Demo/advanced_modal_demo.py +275 -0
  89. dars/templates/examples/advanced/SimpleDashboard/dashboard.py +437 -0
  90. dars/templates/examples/advanced/SimpleModermWeb/modern_web_app.py +452 -0
  91. dars/templates/examples/advanced/VariousComponents/all_components_demo.py +87 -0
  92. dars/templates/examples/advanced/__init__.py +0 -0
  93. dars/templates/examples/advanced/dState/state_mods_demo.py +68 -0
  94. dars/templates/examples/basic/Forms/form_components.py +516 -0
  95. dars/templates/examples/basic/Forms/simple_form.py +379 -0
  96. dars/templates/examples/basic/HelloWorld/hello_world.py +56 -0
  97. dars/templates/examples/basic/Layouts/flex_layout_responsive.py +13 -0
  98. dars/templates/examples/basic/Layouts/grid_layout_responsive.py +12 -0
  99. dars/templates/examples/basic/Layouts/layout_multipage_demo.py +23 -0
  100. dars/templates/examples/basic/Multipage/multipage_example.py +67 -0
  101. dars/templates/examples/basic/PWA/icon-192x192.png +0 -0
  102. dars/templates/examples/basic/PWA/icon-512x512.png +0 -0
  103. dars/templates/examples/basic/PWA/pwa_custom_icons.py +33 -0
  104. dars/templates/examples/basic/__init__.py +0 -0
  105. dars/templates/examples/demo/__pycache__/complete_app.cpython-311.pyc +0 -0
  106. dars/templates/examples/demo/complete_app.py +21 -0
  107. dars/templates/examples/markdown/MarkdownTemplate/README.md +159 -0
  108. dars/templates/examples/markdown/MarkdownTemplate/markdown_template.py +21 -0
  109. dars/templates/examples/markdown/MarkdownTemplate/other_docs.md +1 -0
  110. dars/templates/examples/markdown/__init__.py +0 -0
  111. dars/templates/html/__init__.py +0 -0
  112. dars/version.py +2 -0
  113. dars_framework-1.2.3.dist-info/METADATA +15 -0
  114. dars_framework-1.2.3.dist-info/RECORD +118 -0
  115. dars_framework-1.2.3.dist-info/WHEEL +5 -0
  116. dars_framework-1.2.3.dist-info/entry_points.txt +2 -0
  117. dars_framework-1.2.3.dist-info/licenses/LICENSE +21 -0
  118. dars_framework-1.2.3.dist-info/top_level.txt +1 -0
dars/core/state.py ADDED
@@ -0,0 +1,309 @@
1
+ from typing import Any, Dict, List, Optional
2
+ from dars.scripts.script import InlineScript
3
+
4
+ # Global registry collected at authoring time (Python)
5
+ STATE_BOOTSTRAP: List[Dict[str, Any]] = []
6
+
7
+ class DarsState:
8
+ def __init__(self, name: str, id: Optional[str], states: Optional[List[Any]], is_custom: bool = False):
9
+ self.name = name
10
+ self.id = id
11
+ self.states = states or []
12
+ self.is_custom = is_custom
13
+ self.rules: Dict[str, Dict[str, Any]] = {}
14
+ self._bootstrap_ref: Optional[Dict[str, Any]] = None
15
+
16
+ def to_dict(self) -> Dict[str, Any]:
17
+ d = {
18
+ "name": self.name,
19
+ "id": self.id,
20
+ "states": self.states,
21
+ "isCustom": self.is_custom,
22
+ }
23
+ try:
24
+ d["defaultIndex"] = 0
25
+ d["defaultValue"] = (self.states[0] if isinstance(self.states, list) and len(self.states) > 0 else None)
26
+ except Exception:
27
+ d["defaultIndex"] = 0
28
+ d["defaultValue"] = None
29
+ if self.rules:
30
+ d["rules"] = self.rules
31
+ return d
32
+
33
+ def state(self, idx: Optional[int] = None, cComp: bool = False, render: Optional[Any] = None, goto: Optional[Any] = None) -> InlineScript:
34
+ """
35
+ Convenience: returns an InlineScript that, when added to a page/app,
36
+ triggers a state change via the JS runtime. Intended for quick prototyping.
37
+
38
+ - idx: target state index/value
39
+ - cComp: if True, performs a full HTML replace (custom component flow)
40
+ - render: HTML string to inject when cComp=True
41
+ """
42
+ target_id = self.id or ""
43
+
44
+ def _escape_js_str(s: str) -> str:
45
+ return s.replace("\\", "\\\\").replace("'", "\\'").replace("\n", "\\n").replace("\r", "")
46
+
47
+ # Compute HTML if needed
48
+ html_val = None
49
+ if cComp and render is not None:
50
+ try:
51
+ # DeferredAttr -> clone component with attrs
52
+ if hasattr(render, 'clone_with') and callable(getattr(render, 'clone_with')):
53
+ render = render.clone_with()
54
+ # If it's a Component instance, render it to HTML
55
+ try:
56
+ from dars.core.component import Component as _DarsComponent
57
+ if isinstance(render, _DarsComponent):
58
+ from dars.exporters.web.html_css_js import HTMLCSSJSExporter
59
+ _exp = HTMLCSSJSExporter()
60
+ html_val = _exp.render_component(render)
61
+ except Exception:
62
+ html_val = None
63
+ if html_val is None and isinstance(render, str):
64
+ html_val = render
65
+ except Exception:
66
+ html_val = None
67
+
68
+ # Build payload
69
+ parts = [f"id: '{_escape_js_str(target_id)}'", f"name: '{_escape_js_str(self.name)}'"]
70
+ if idx is not None:
71
+ parts.append(f"state: {idx}")
72
+ if goto is not None:
73
+ if isinstance(goto, str):
74
+ parts.append(f"goto: '{_escape_js_str(goto)}'")
75
+ else:
76
+ parts.append(f"goto: {goto}")
77
+ if cComp:
78
+ html_str = _escape_js_str(html_val or "")
79
+ parts.append("useCustomRender: true")
80
+ parts.append(f"html: '{html_str}'")
81
+ payload = ", ".join(parts)
82
+
83
+ code = (
84
+ "(async () => {\n"
85
+ " try {\n"
86
+ " let ch = window.__DARS_CHANGE_FN;\n"
87
+ " if (!ch) {\n"
88
+ " if (window.Dars && typeof window.Dars.change === 'function') {\n"
89
+ " ch = window.Dars.change.bind(window.Dars);\n"
90
+ " } else {\n"
91
+ " const m = await import('./lib/dars.min.js');\n"
92
+ " ch = (m.change || (m.default && m.default.change));\n"
93
+ " }\n"
94
+ " if (typeof ch === 'function') window.__DARS_CHANGE_FN = ch;\n"
95
+ " }\n"
96
+ f" if (typeof ch === 'function') ch({{{payload}}});\n"
97
+ " } catch (e) { /* noop */ }\n"
98
+ "})();\n"
99
+ )
100
+ return InlineScript(code, module=True)
101
+
102
+ # --- cState: define rules/mods for a given state index ---
103
+ def cState(self, idx: int, mods: Optional[List[Dict[str, Any]]] = None) -> 'CStateRuleBuilder':
104
+ key = str(idx)
105
+ if idx == 0:
106
+ raise ValueError(
107
+ "Default state (index 0) is immutable. Do not define cState(0). "
108
+ "Configure the component's default directly on the instance instead."
109
+ )
110
+ if key not in self.rules:
111
+ self.rules[key] = {}
112
+ if mods:
113
+ existing = list(self.rules[key].get('mods', []))
114
+ existing.extend(mods)
115
+ self.rules[key]['mods'] = existing
116
+ # mirror into bootstrap ref if exists
117
+ if self._bootstrap_ref is not None:
118
+ self._bootstrap_ref.setdefault('rules', {})
119
+ self._bootstrap_ref['rules'][key] = self.rules[key]
120
+ return CStateRuleBuilder(self, key)
121
+
122
+ # sugar: direct goto builder for rules
123
+ def goto(self, value: Any) -> 'CStateRuleBuilder':
124
+ # attach as default rule for current state if exists, else for state 0
125
+ key = str(0)
126
+ if key not in self.rules:
127
+ self.rules[key] = {}
128
+ self.rules[key]['goto'] = value
129
+ if self._bootstrap_ref is not None:
130
+ self._bootstrap_ref.setdefault('rules', {})
131
+ self._bootstrap_ref['rules'][key] = self.rules[key]
132
+ return CStateRuleBuilder(self, key)
133
+
134
+
135
+ class Mod:
136
+ @staticmethod
137
+ def inc(target: Any, prop: str = 'text', by: int = 1) -> Dict[str, Any]:
138
+ tid = getattr(target, 'id', None) or str(target)
139
+ return {"op": "inc", "target": tid, "prop": prop, "by": by}
140
+
141
+ @staticmethod
142
+ def dec(target: Any, prop: str = 'text', by: int = 1) -> Dict[str, Any]:
143
+ return Mod.inc(target, prop=prop, by=(-abs(by)))
144
+
145
+ @staticmethod
146
+ def set(target: Any, **attrs) -> Dict[str, Any]:
147
+ tid = getattr(target, 'id', None) or str(target)
148
+ return {"op": "set", "target": tid, "attrs": attrs}
149
+
150
+ @staticmethod
151
+ def toggle_class(target: Any, name: str, on: Optional[bool] = None) -> Dict[str, Any]:
152
+ tid = getattr(target, 'id', None) or str(target)
153
+ d: Dict[str, Any] = {"op": "toggleClass", "target": tid, "name": name}
154
+ if on is not None:
155
+ d['on'] = bool(on)
156
+ return d
157
+
158
+ @staticmethod
159
+ def append_text(target: Any, value: str) -> Dict[str, Any]:
160
+ tid = getattr(target, 'id', None) or str(target)
161
+ return {"op": "appendText", "target": tid, "value": value}
162
+
163
+ @staticmethod
164
+ def prepend_text(target: Any, value: str) -> Dict[str, Any]:
165
+ tid = getattr(target, 'id', None) or str(target)
166
+ return {"op": "prependText", "target": tid, "value": value}
167
+
168
+ @staticmethod
169
+ def call(target: Any, state: Any = None, goto: Any = None) -> Dict[str, Any]:
170
+ """Invoke another dState's state change.
171
+ - target: DarsState instance or state name string; if a component is passed, use its id.
172
+ - state: target state index/value.
173
+ - goto: relative/absolute goto directive (e.g., '+1').
174
+ The runtime will resolve the state by name first (registry), falling back to id.
175
+ """
176
+ name: Optional[str] = None
177
+ sid: Optional[str] = None
178
+ try:
179
+ # DarsState instance
180
+ if hasattr(target, 'name') and hasattr(target, 'id'):
181
+ name = getattr(target, 'name', None)
182
+ sid = getattr(target, 'id', None)
183
+ elif isinstance(target, str):
184
+ name = target
185
+ else:
186
+ # Maybe a component; try id
187
+ sid = getattr(target, 'id', None) or str(target)
188
+ except Exception:
189
+ name = None
190
+ sid = None
191
+ d: Dict[str, Any] = {"op": "call"}
192
+ if name:
193
+ d['name'] = name
194
+ if sid:
195
+ d['id'] = sid
196
+ if state is not None:
197
+ d['state'] = state
198
+ if goto is not None:
199
+ d['goto'] = goto
200
+ return d
201
+
202
+
203
+ class CStateRuleBuilder:
204
+ def __init__(self, st: DarsState, key: str):
205
+ self.st = st
206
+ self.key = key
207
+
208
+ def _ensure(self):
209
+ if self.key not in self.st.rules:
210
+ self.st.rules[self.key] = {}
211
+ if 'mods' not in self.st.rules[self.key]:
212
+ self.st.rules[self.key]['mods'] = []
213
+
214
+ def inc(self, target: Any, prop: str = 'text', by: int = 1) -> 'CStateRuleBuilder':
215
+ self._ensure()
216
+ self.st.rules[self.key]['mods'].append(Mod.inc(target, prop, by))
217
+ if self.st._bootstrap_ref is not None:
218
+ self.st._bootstrap_ref.setdefault('rules', {})
219
+ self.st._bootstrap_ref['rules'][self.key] = self.st.rules[self.key]
220
+ return self
221
+
222
+ def dec(self, target: Any, prop: str = 'text', by: int = 1) -> 'CStateRuleBuilder':
223
+ self._ensure()
224
+ self.st.rules[self.key]['mods'].append(Mod.dec(target, prop, by))
225
+ if self.st._bootstrap_ref is not None:
226
+ self.st._bootstrap_ref.setdefault('rules', {})
227
+ self.st._bootstrap_ref['rules'][self.key] = self.st.rules[self.key]
228
+ return self
229
+
230
+ def set(self, target: Any, **attrs) -> 'CStateRuleBuilder':
231
+ self._ensure()
232
+ self.st.rules[self.key]['mods'].append(Mod.set(target, **attrs))
233
+ if self.st._bootstrap_ref is not None:
234
+ self.st._bootstrap_ref.setdefault('rules', {})
235
+ self.st._bootstrap_ref['rules'][self.key] = self.st.rules[self.key]
236
+ return self
237
+
238
+ def toggle_class(self, target: Any, name: str, on: Optional[bool] = None) -> 'CStateRuleBuilder':
239
+ self._ensure()
240
+ self.st.rules[self.key]['mods'].append(Mod.toggle_class(target, name, on))
241
+ if self.st._bootstrap_ref is not None:
242
+ self.st._bootstrap_ref.setdefault('rules', {})
243
+ self.st._bootstrap_ref['rules'][self.key] = self.st.rules[self.key]
244
+ return self
245
+
246
+ def append_text(self, target: Any, value: str) -> 'CStateRuleBuilder':
247
+ self._ensure()
248
+ self.st.rules[self.key]['mods'].append(Mod.append_text(target, value))
249
+ if self.st._bootstrap_ref is not None:
250
+ self.st._bootstrap_ref.setdefault('rules', {})
251
+ self.st._bootstrap_ref['rules'][self.key] = self.st.rules[self.key]
252
+ return self
253
+
254
+ def prepend_text(self, target: Any, value: str) -> 'CStateRuleBuilder':
255
+ self._ensure()
256
+ self.st.rules[self.key]['mods'].append(Mod.prepend_text(target, value))
257
+ if self.st._bootstrap_ref is not None:
258
+ self.st._bootstrap_ref.setdefault('rules', {})
259
+ self.st._bootstrap_ref['rules'][self.key] = self.st.rules[self.key]
260
+ return self
261
+
262
+ def call(self, target: Any, state: Any = None, goto: Any = None) -> 'CStateRuleBuilder':
263
+ """Append a cross-state call op to this rule."""
264
+ self._ensure()
265
+ self.st.rules[self.key]['mods'].append(Mod.call(target, state=state, goto=goto))
266
+ if self.st._bootstrap_ref is not None:
267
+ self.st._bootstrap_ref.setdefault('rules', {})
268
+ self.st._bootstrap_ref['rules'][self.key] = self.st.rules[self.key]
269
+ return self
270
+
271
+ def goto(self, value: Any) -> 'CStateRuleBuilder':
272
+ if self.key not in self.st.rules:
273
+ self.st.rules[self.key] = {}
274
+ self.st.rules[self.key]['goto'] = value
275
+ if self.st._bootstrap_ref is not None:
276
+ self.st._bootstrap_ref.setdefault('rules', {})
277
+ self.st._bootstrap_ref['rules'][self.key] = self.st.rules[self.key]
278
+ return self
279
+
280
+
281
+ def dState(name: str, component: Any = None, id: Optional[str] = None, states: Optional[List[Any]] = None, is_custom: bool = False) -> DarsState:
282
+ """
283
+ Declare a state associated with a component or an element id.
284
+ - name: state name (unique enough per app).
285
+ - component: a Dars component instance; if provided and has .id, it is used.
286
+ - id: explicit target id when component is not provided.
287
+ - states: optional list of possible state values (metadata).
288
+ - is_custom: mark as custom component to indicate full HTML replace flows.
289
+
290
+ Returns a DarsState object (for ergonomics), and records the state
291
+ in a global bootstrap list consumed by the exporter.
292
+ """
293
+ target_id = None
294
+ try:
295
+ if component is not None and hasattr(component, 'id'):
296
+ target_id = getattr(component, 'id')
297
+ except Exception:
298
+ target_id = None
299
+ if not target_id:
300
+ target_id = id
301
+
302
+ st = DarsState(name=name, id=target_id, states=states, is_custom=is_custom)
303
+ try:
304
+ d = st.to_dict()
305
+ STATE_BOOTSTRAP.append(d)
306
+ st._bootstrap_ref = d
307
+ except Exception:
308
+ pass
309
+ return st
@@ -0,0 +1,56 @@
1
+ from dars.all import *
2
+
3
+ app = App(title="Dars-HealthCheck", theme="dark")
4
+ # Crear componentes
5
+ container = Container(
6
+ Text(
7
+ text="Dars Health Check",
8
+ style={
9
+ 'font-size': '48px',
10
+ 'color': '#2c3e50',
11
+ 'margin-bottom': '20px',
12
+ 'font-weight': 'bold',
13
+ 'text-align': 'center'
14
+ }
15
+ ),
16
+ Text(
17
+ text="Dars Health Check",
18
+ style={
19
+ 'font-size': '20px',
20
+ 'color': '#7f8c8d',
21
+ 'margin-bottom': '40px',
22
+ 'text-align': 'center'
23
+ }
24
+ ),
25
+
26
+ Button(
27
+ text="Check",
28
+ on_click= dScript("""alert('Health Check')"""),
29
+ on_mouse_enter=dScript("""this.style.backgroundColor = '#2980b9';"""),
30
+ on_mouse_leave=dScript("""this.style.backgroundColor = '#3498db';"""),
31
+ style={
32
+ 'background-color': '#3498db',
33
+ 'color': 'white',
34
+ 'padding': '15px 30px',
35
+ 'border': 'none',
36
+ 'border-radius': '8px',
37
+ 'font-size': '18px',
38
+ 'cursor': 'pointer',
39
+ 'transition': 'background-color 0.3s'
40
+ }
41
+ ),
42
+ style={
43
+ 'display': 'flex',
44
+ 'flex-direction': 'column',
45
+ 'align-items': 'center',
46
+ 'justify-content': 'center',
47
+ 'min-height': '100vh',
48
+ 'background-color': '#f0f2f5',
49
+ 'font-family': 'Arial, sans-serif'
50
+ }
51
+ )
52
+
53
+ app.set_root(container)
54
+
55
+ if __name__ == "__main__":
56
+ app.rTimeCompile()
@@ -0,0 +1,275 @@
1
+ # Runner that executes the test files and prints a colored report using rich
2
+ import runpy, glob, importlib.util, os, sys, traceback, subprocess, time, threading
3
+ import requests
4
+ import signal
5
+ import webbrowser
6
+ import shutil
7
+ from rich.console import Console
8
+ from rich.table import Table
9
+ from rich.panel import Panel
10
+ from rich.prompt import Prompt
11
+
12
+ console = Console()
13
+
14
+ def run_unit_tests(unit_test_paths=None):
15
+ """Run unit test files, optionally from specific paths"""
16
+ if unit_test_paths is None:
17
+ # Default behavior: run all tests in tests directory
18
+ tests = sorted(glob.glob(os.path.join(os.path.dirname(__file__), 'tests', 'test_*.py')))
19
+ else:
20
+ # Use provided test paths
21
+ tests = unit_test_paths
22
+
23
+ results = []
24
+ for t in tests:
25
+ name = os.path.basename(t)
26
+ try:
27
+ console.print(Panel(f'Running {name}', style='cyan'))
28
+ ns = runpy.run_path(t, run_name='__main__')
29
+ results.append((name, True, None))
30
+ console.print(f'[green]PASS[/green] {name}')
31
+ except Exception as e:
32
+ results.append((name, False, traceback.format_exc()))
33
+ console.print(f'[red]FAIL[/red] {name}')
34
+ console.print(traceback.format_exc())
35
+ return results
36
+
37
+ def check_server(port=8000, timeout=10):
38
+ """Check if the server is running on the given port"""
39
+ start_time = time.time()
40
+ while time.time() - start_time < timeout:
41
+ try:
42
+ response = requests.get(f'http://localhost:{port}', timeout=2)
43
+ if response.status_code < 500:
44
+ return True
45
+ except requests.exceptions.RequestException:
46
+ time.sleep(0.5)
47
+ return False
48
+
49
+ def run_app_with_timeout(app_file, timeout=15):
50
+ """Run a Dars app with rTimeCompile and capture its output with a timeout"""
51
+ try:
52
+ env = os.environ.copy()
53
+ env['PYTHONIOENCODING'] = 'utf-8'
54
+
55
+ process = subprocess.Popen(
56
+ [sys.executable, app_file],
57
+ stdout=subprocess.PIPE,
58
+ stderr=subprocess.PIPE,
59
+ cwd=os.path.dirname(app_file) or None, # evita cwd = "" en Windows
60
+ env=env,
61
+ )
62
+
63
+ # Esperar a que el servidor se inicie o timeout
64
+ server_started = False
65
+ start_time = time.time()
66
+
67
+ while time.time() - start_time < timeout:
68
+ if check_server(8000, 2):
69
+ server_started = True
70
+ break
71
+ # Verificar si el proceso ha terminado (lo que indicaría un error)
72
+ if process.poll() is not None:
73
+ break
74
+ time.sleep(1)
75
+
76
+ # Recoger la salida (aunque no haya terminado, para capturar errores iniciales)
77
+ try:
78
+ stdout, stderr = process.communicate(timeout=2)
79
+ except subprocess.TimeoutExpired:
80
+ stdout, stderr = b"", b""
81
+
82
+ stdout_str = stdout.decode('utf-8', errors='replace') if stdout else ''
83
+ stderr_str = stderr.decode('utf-8', errors='replace') if stderr else ''
84
+
85
+ return {
86
+ 'success': server_started,
87
+ 'process': process,
88
+ 'stdout': stdout_str,
89
+ 'stderr': stderr_str
90
+ }
91
+
92
+ except Exception as e:
93
+ return {
94
+ 'success': False,
95
+ 'error': str(e),
96
+ 'stdout': '',
97
+ 'stderr': ''
98
+ }
99
+
100
+
101
+ def safe_read_file(file_path):
102
+ """Leer un archivo de forma segura manejando diferentes codificaciones"""
103
+ encodings = ['utf-8', 'latin-1', 'cp1252', 'iso-8859-1']
104
+
105
+ for encoding in encodings:
106
+ try:
107
+ with open(file_path, 'r', encoding=encoding) as f:
108
+ return f.read()
109
+ except UnicodeDecodeError:
110
+ continue
111
+
112
+ # Si todas las codificaciones fallan, usar modo binario con reemplazo de errores
113
+ with open(file_path, 'rb') as f:
114
+ return f.read().decode('utf-8', errors='replace')
115
+
116
+ def run_app_tests(app_test_paths=None):
117
+ """Run Dars application tests that use rTimeCompile, optionally from specific paths"""
118
+ if app_test_paths is None:
119
+ # Default behavior: run all apps in apps_test directory
120
+ apps_test_dir = os.path.join(os.path.dirname(__file__), 'apps_test')
121
+ if not os.path.exists(apps_test_dir):
122
+ console.print(f"[yellow]apps_test directory not found: {apps_test_dir}[/yellow]")
123
+ return []
124
+
125
+ apps = sorted(glob.glob(os.path.join(apps_test_dir, '*.py')))
126
+ else:
127
+ # Use provided app paths
128
+ apps = app_test_paths
129
+
130
+ results = []
131
+
132
+ for app_file in apps:
133
+ name = os.path.basename(app_file)
134
+ try:
135
+ console.print(Panel(f'Testing Dars App: {name}', style='magenta'))
136
+
137
+ # Verificar si la aplicación usa rTimeCompile de forma segura
138
+ content = safe_read_file(app_file)
139
+ uses_rTimeCompile = 'rTimeCompile' in content
140
+
141
+ if not uses_rTimeCompile:
142
+ console.print(f"[yellow]Skipping {name} - does not use rTimeCompile[/yellow]")
143
+ continue
144
+
145
+ # Ejecutar la aplicación con timeout
146
+ result = run_app_with_timeout(app_file, timeout=20)
147
+
148
+ if result['success']:
149
+ # Éxito: el servidor se inició correctamente
150
+ results.append((name, True, "App started server successfully on port 8000"))
151
+ console.print(f'[green]PASS[/green] {name}')
152
+
153
+ # Abrir el navegador para verificación visual
154
+ console.print("[green]Opening browser for visual verification...[/green]")
155
+ webbrowser.open('http://localhost:8000')
156
+
157
+ # Preguntar al usuario si quiere terminar el proceso
158
+ console.print("\n[bold]Please verify the application in your browser.[/bold]")
159
+ console.print("After verification, you can:")
160
+ console.print("1. Press 'y' to terminate the process and continue with tests")
161
+ console.print("2. Press 'n' to keep the server running and continue")
162
+
163
+ response = Prompt.ask("\nTerminate process and continue?", choices=["y", "n"], default="y")
164
+
165
+ if response.lower() == "y":
166
+ # Terminar el proceso
167
+ if 'process' in result and result['process'].poll() is None:
168
+ console.print("[yellow]Terminating app process...[/yellow]")
169
+ try:
170
+ # Enviar señal de interrupción (equivalente a Ctrl+C)
171
+ if os.name == 'nt': # Windows
172
+ result['process'].terminate()
173
+ else: # Unix
174
+ result['process'].send_signal(signal.SIGINT)
175
+
176
+ # Esperar a que termine
177
+ result['process'].wait(timeout=10)
178
+ console.print("[green]App process terminated successfully[/green]")
179
+
180
+ # Limpiar carpeta dars_preview
181
+ preview_dir = os.path.join(os.path.dirname(app_file), "dars_preview")
182
+ if os.path.exists(preview_dir):
183
+ shutil.rmtree(preview_dir)
184
+ console.print("[green]Preview directory cleaned up[/green]")
185
+
186
+ except:
187
+ console.print("[red]Force killing app process...[/red]")
188
+ result['process'].kill()
189
+ else:
190
+ console.print("[yellow]Keeping server running...[/yellow]")
191
+ else:
192
+ # Fallo: el servidor no se inició
193
+ error_msg = result['stderr'] or result['stdout'] or "Unknown error"
194
+ results.append((name, False, error_msg))
195
+ console.print(f'[red]FAIL[/red] {name}: {error_msg}')
196
+
197
+ # Terminar el proceso si todavía está ejecutándose (solo si no elegimos mantenerlo)
198
+ if 'process' in result and result['process'].poll() is None:
199
+ console.print("[yellow]Terminating app process...[/yellow]")
200
+ try:
201
+ # Enviar señal de interrupción (equivalente a Ctrl+C)
202
+ if os.name == 'nt': # Windows
203
+ result['process'].terminate()
204
+ else: # Unix
205
+ result['process'].send_signal(signal.SIGINT)
206
+
207
+ # Esperar a que termine
208
+ result['process'].wait(timeout=10)
209
+ console.print("[green]App process terminated successfully[/green]")
210
+
211
+ # Limpiar carpeta dars_preview
212
+ preview_dir = os.path.join(os.path.dirname(app_file), "dars_preview")
213
+ if os.path.exists(preview_dir):
214
+ shutil.rmtree(preview_dir)
215
+ console.print("[green]Preview directory cleaned up[/green]")
216
+
217
+ except:
218
+ console.print("[red]Force killing app process...[/red]")
219
+ result['process'].kill()
220
+
221
+ except Exception as e:
222
+ results.append((name, False, str(e)))
223
+ console.print(f'[red]FAIL[/red] {name}: {e}')
224
+
225
+ return results
226
+
227
+ def main(unit_test_paths=None, app_test_paths=None):
228
+ """
229
+ Run Dars test suite with optional specific test paths
230
+
231
+ Args:
232
+ unit_test_paths (list): Optional list of paths to unit test files
233
+ app_test_paths (list): Optional list of paths to application test files
234
+ """
235
+ # Run unit tests
236
+ console.print(Panel("Running Unit Tests", style="cyan"))
237
+ unit_results = run_unit_tests(unit_test_paths)
238
+
239
+ # Run app tests
240
+ console.print(Panel("Running App Tests (rTimeCompile)", style="magenta"))
241
+ app_results = run_app_tests(app_test_paths)
242
+
243
+ # Combine results
244
+ all_results = unit_results + app_results
245
+
246
+ # Print results table
247
+ table = Table(title='Dars Test Suite Results')
248
+ table.add_column('Test File')
249
+ table.add_column('Result')
250
+ table.add_column('Details', overflow='fold')
251
+
252
+ for name, ok, details in all_results:
253
+ table.add_row(name, '[green]PASS[/green]' if ok else '[red]FAIL[/red]', details or '')
254
+
255
+ console.print(table)
256
+
257
+ # Exit code
258
+ any_fail = any(not ok for _, ok, _ in all_results)
259
+ if any_fail:
260
+ console.print('[bold red]Some tests failed.[/bold red]')
261
+ sys.exit(1)
262
+ else:
263
+ console.print('[bold green]All tests passed.[/bold green]')
264
+
265
+ if __name__ == '__main__':
266
+ # Parse command line arguments for custom test paths
267
+ import argparse
268
+ parser = argparse.ArgumentParser(description='Run Dars Framework tests')
269
+ parser.add_argument('--unit-tests', nargs='+', help='Paths to specific unit test files')
270
+ parser.add_argument('--app-tests', nargs='+', help='Paths to specific application test files')
271
+
272
+ args = parser.parse_args()
273
+
274
+ # Run tests with optional paths
275
+ main(unit_test_paths=args.unit_tests, app_test_paths=args.app_tests)