pywire 0.1.1__py3-none-any.whl → 0.1.2__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 (101) hide show
  1. pywire/__init__.py +2 -0
  2. pywire/cli/__init__.py +1 -0
  3. pywire/cli/generators.py +48 -0
  4. pywire/cli/main.py +309 -0
  5. pywire/cli/tui.py +563 -0
  6. pywire/cli/validate.py +26 -0
  7. pywire/client/.prettierignore +8 -0
  8. pywire/client/.prettierrc +7 -0
  9. pywire/client/build.mjs +73 -0
  10. pywire/client/eslint.config.js +46 -0
  11. pywire/client/package.json +39 -0
  12. pywire/client/pnpm-lock.yaml +2971 -0
  13. pywire/client/src/core/app.ts +263 -0
  14. pywire/client/src/core/dom-updater.test.ts +78 -0
  15. pywire/client/src/core/dom-updater.ts +321 -0
  16. pywire/client/src/core/index.ts +5 -0
  17. pywire/client/src/core/transport-manager.test.ts +179 -0
  18. pywire/client/src/core/transport-manager.ts +159 -0
  19. pywire/client/src/core/transports/base.ts +122 -0
  20. pywire/client/src/core/transports/http.ts +142 -0
  21. pywire/client/src/core/transports/index.ts +13 -0
  22. pywire/client/src/core/transports/websocket.ts +97 -0
  23. pywire/client/src/core/transports/webtransport.ts +149 -0
  24. pywire/client/src/dev/dev-app.ts +93 -0
  25. pywire/client/src/dev/error-trace.test.ts +97 -0
  26. pywire/client/src/dev/error-trace.ts +76 -0
  27. pywire/client/src/dev/index.ts +4 -0
  28. pywire/client/src/dev/status-overlay.ts +63 -0
  29. pywire/client/src/events/handler.test.ts +318 -0
  30. pywire/client/src/events/handler.ts +454 -0
  31. pywire/client/src/pywire.core.ts +22 -0
  32. pywire/client/src/pywire.dev.ts +27 -0
  33. pywire/client/tsconfig.json +17 -0
  34. pywire/client/vitest.config.ts +15 -0
  35. pywire/compiler/__init__.py +6 -0
  36. pywire/compiler/ast_nodes.py +304 -0
  37. pywire/compiler/attributes/__init__.py +6 -0
  38. pywire/compiler/attributes/base.py +24 -0
  39. pywire/compiler/attributes/conditional.py +37 -0
  40. pywire/compiler/attributes/events.py +55 -0
  41. pywire/compiler/attributes/form.py +37 -0
  42. pywire/compiler/attributes/loop.py +75 -0
  43. pywire/compiler/attributes/reactive.py +34 -0
  44. pywire/compiler/build.py +28 -0
  45. pywire/compiler/build_artifacts.py +342 -0
  46. pywire/compiler/codegen/__init__.py +5 -0
  47. pywire/compiler/codegen/attributes/__init__.py +6 -0
  48. pywire/compiler/codegen/attributes/base.py +19 -0
  49. pywire/compiler/codegen/attributes/events.py +35 -0
  50. pywire/compiler/codegen/directives/__init__.py +6 -0
  51. pywire/compiler/codegen/directives/base.py +16 -0
  52. pywire/compiler/codegen/directives/path.py +53 -0
  53. pywire/compiler/codegen/generator.py +2341 -0
  54. pywire/compiler/codegen/template.py +2178 -0
  55. pywire/compiler/directives/__init__.py +7 -0
  56. pywire/compiler/directives/base.py +20 -0
  57. pywire/compiler/directives/component.py +33 -0
  58. pywire/compiler/directives/context.py +93 -0
  59. pywire/compiler/directives/layout.py +49 -0
  60. pywire/compiler/directives/no_spa.py +24 -0
  61. pywire/compiler/directives/path.py +71 -0
  62. pywire/compiler/directives/props.py +88 -0
  63. pywire/compiler/exceptions.py +19 -0
  64. pywire/compiler/interpolation/__init__.py +6 -0
  65. pywire/compiler/interpolation/base.py +28 -0
  66. pywire/compiler/interpolation/jinja.py +272 -0
  67. pywire/compiler/parser.py +750 -0
  68. pywire/compiler/paths.py +29 -0
  69. pywire/compiler/preprocessor.py +43 -0
  70. pywire/core/wire.py +119 -0
  71. pywire/py.typed +0 -0
  72. pywire/runtime/__init__.py +7 -0
  73. pywire/runtime/aioquic_server.py +194 -0
  74. pywire/runtime/app.py +901 -0
  75. pywire/runtime/compile_error_page.py +195 -0
  76. pywire/runtime/debug.py +203 -0
  77. pywire/runtime/dev_server.py +434 -0
  78. pywire/runtime/dev_server.py.broken +268 -0
  79. pywire/runtime/error_page.py +64 -0
  80. pywire/runtime/error_renderer.py +23 -0
  81. pywire/runtime/escape.py +23 -0
  82. pywire/runtime/files.py +40 -0
  83. pywire/runtime/helpers.py +97 -0
  84. pywire/runtime/http_transport.py +253 -0
  85. pywire/runtime/loader.py +272 -0
  86. pywire/runtime/logging.py +72 -0
  87. pywire/runtime/page.py +384 -0
  88. pywire/runtime/pydantic_integration.py +52 -0
  89. pywire/runtime/router.py +229 -0
  90. pywire/runtime/server.py +25 -0
  91. pywire/runtime/style_collector.py +31 -0
  92. pywire/runtime/upload_manager.py +76 -0
  93. pywire/runtime/validation.py +449 -0
  94. pywire/runtime/websocket.py +665 -0
  95. pywire/runtime/webtransport_handler.py +195 -0
  96. {pywire-0.1.1.dist-info → pywire-0.1.2.dist-info}/METADATA +1 -1
  97. pywire-0.1.2.dist-info/RECORD +104 -0
  98. pywire-0.1.1.dist-info/RECORD +0 -9
  99. {pywire-0.1.1.dist-info → pywire-0.1.2.dist-info}/WHEEL +0 -0
  100. {pywire-0.1.1.dist-info → pywire-0.1.2.dist-info}/entry_points.txt +0 -0
  101. {pywire-0.1.1.dist-info → pywire-0.1.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,195 @@
1
+ import html
2
+ import linecache
3
+ import os
4
+ import traceback
5
+ from typing import Any, Dict, Optional, Union, cast
6
+
7
+ from starlette.requests import Request
8
+ from starlette.responses import HTMLResponse
9
+
10
+ from pywire.compiler.exceptions import PyWireSyntaxError
11
+ from pywire.runtime.page import BasePage
12
+
13
+
14
+ class CompileErrorPage(BasePage):
15
+ """Page used to display compilation errors with helpful context.
16
+
17
+ Handles both PyWireSyntaxError (with known file/line) and generic
18
+ exceptions (extracting info from traceback).
19
+ """
20
+
21
+ def __init__(
22
+ self,
23
+ request: Request,
24
+ error: Union[PyWireSyntaxError, Exception],
25
+ file_path: Optional[str] = None,
26
+ ):
27
+ self.request = request
28
+ self.error = error
29
+ self._file_path = file_path
30
+ self.error_file: Optional[str] = None
31
+ self.error_line: Optional[int] = None
32
+
33
+ # Extract file/line info based on error type
34
+ if isinstance(error, PyWireSyntaxError):
35
+ self.error_file = error.file_path
36
+ self.error_line = error.line
37
+ self.error_message = error.message
38
+ self.traceback_lines = None # No traceback for syntax errors
39
+ else:
40
+ # Generic exception - extract from traceback
41
+ self.error_message = f"{type(error).__name__}: {str(error)}"
42
+ self.traceback_lines = traceback.format_exception(
43
+ type(error), error, error.__traceback__
44
+ )
45
+
46
+ # Try to find the most relevant frame (last user code frame)
47
+ self.error_file = file_path
48
+ self.error_line = None
49
+ if error.__traceback__:
50
+ tb_summary = traceback.extract_tb(error.__traceback__)
51
+ for frame in reversed(tb_summary):
52
+ # Prefer .pywire files
53
+ if frame.filename.endswith(".pywire"):
54
+ self.error_file = frame.filename
55
+ self.error_line = frame.lineno
56
+ break
57
+ # Otherwise use last non-framework frame
58
+ if (
59
+ "pywire/src/pywire" not in frame.filename
60
+ and "site-packages" not in frame.filename
61
+ ):
62
+ self.error_file = frame.filename
63
+ self.error_line = frame.lineno
64
+ break
65
+ # Fallback to last frame if nothing better found
66
+ if self.error_line is None and tb_summary:
67
+ self.error_file = tb_summary[-1].filename
68
+ self.error_line = tb_summary[-1].lineno
69
+
70
+ async def render(self, init: bool = True) -> HTMLResponse:
71
+ """Render the compile error page."""
72
+ # Read the context around the error line
73
+ context_lines = []
74
+ if self.error_file and self.error_line and os.path.exists(self.error_file):
75
+ try:
76
+ # Force cache update
77
+ linecache.checkcache(self.error_file)
78
+ lines = linecache.getlines(self.error_file)
79
+ start = max(1, self.error_line - 5)
80
+ end = min(len(lines), self.error_line + 5)
81
+
82
+ for i in range(start, end + 1):
83
+ if i <= len(lines):
84
+ context_lines.append(
85
+ {
86
+ "num": i,
87
+ "content": lines[i - 1].rstrip(),
88
+ "is_current": i == self.error_line,
89
+ }
90
+ )
91
+ except Exception:
92
+ pass
93
+
94
+ # Generate code context HTML
95
+ context_html = ""
96
+ for line in context_lines:
97
+ content = cast(str, line["content"])
98
+ cls = "line-current" if line["is_current"] else "line"
99
+ context_html += (
100
+ f"<div class='{cls}'><span class='line-num'>{line['num']}</span> "
101
+ f"<span class='code'>{html.escape(content)}</span></div>"
102
+ )
103
+
104
+ # Shorten file path for display
105
+ file_display = self.error_file or "unknown"
106
+ try:
107
+ cwd = os.getcwd()
108
+ if file_display.startswith(cwd):
109
+ file_display = os.path.relpath(file_display, cwd)
110
+ except Exception:
111
+ pass
112
+
113
+ # Error title based on type
114
+ if isinstance(self.error, PyWireSyntaxError):
115
+ error_title = "PyWire Syntax Error"
116
+ else:
117
+ error_title = "Compilation Error"
118
+
119
+ # Traceback section for generic exceptions
120
+ traceback_html = ""
121
+ if self.traceback_lines:
122
+ tb_text = "".join(self.traceback_lines)
123
+ traceback_html = f"""
124
+ <div class="traceback-section">
125
+ <h3>Full Traceback</h3>
126
+ <pre class="traceback">{html.escape(tb_text)}</pre>
127
+ </div>
128
+ """
129
+
130
+ content = f"""
131
+ <!DOCTYPE html>
132
+ <html>
133
+ <head>
134
+ <title>{error_title}</title>
135
+ <style>
136
+ body {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI",
137
+ Roboto, sans-serif;
138
+ background: #1a1a1a; color: #e0e0e0; margin: 0; padding: 20px; }}
139
+ h1 {{ color: #ff6b6b; font-size: 24px; margin-bottom: 5px; }}
140
+ h3 {{ color: #aaa; font-size: 16px; margin-top: 30px; margin-bottom: 10px; }}
141
+ .exc-msg {{ font-size: 16px; color: #fff; margin-bottom: 20px;
142
+ white-space: pre-wrap; font-family: monospace;
143
+ line-height: 1.6; }}
144
+ .container {{ max-width: 1000px; margin: 0 auto; }}
145
+ .error-location {{ background: #2d2d2d; border-radius: 8px; padding: 15px;
146
+ margin-bottom: 20px; border-left: 4px solid #ff6b6b; }}
147
+ .file-info {{ color: #ffd43b; font-family: monospace; font-size: 14px;
148
+ margin-bottom: 10px; }}
149
+ .code-context {{ padding: 10px 0; background: #222; font-family: "Fira Code",
150
+ monospace; font-size: 13px; overflow-x: auto; border-radius: 4px; }}
151
+ .line {{ padding: 2px 15px; color: #888; display: flex; }}
152
+ .line-current {{ padding: 2px 15px; background: #3c1e1e; color: #ffcccc;
153
+ display: flex; border-left: 3px solid #ff6b6b; }}
154
+ .line-num {{ width: 40px; text-align: right; margin-right: 15px; opacity: 0.5;
155
+ user-select: none; }}
156
+ .code {{ white-space: pre; }}
157
+ .traceback-section {{ margin-top: 20px; }}
158
+ .traceback {{ background: #222; padding: 15px; border-radius: 8px; font-size: 12px;
159
+ overflow-x: auto; color: #ccc; white-space: pre-wrap;
160
+ word-break: break-word; }}
161
+ .header-block {{ border-bottom: 1px solid #333; padding-bottom: 20px;
162
+ margin-bottom: 20px; }}
163
+ </style>
164
+ </head>
165
+ <body>
166
+ <div class="container">
167
+ <div class="header-block">
168
+ <h1>{error_title}</h1>
169
+ </div>
170
+
171
+ <div class="error-location">
172
+ <div class="file-info">{html.escape(file_display)}{
173
+ ":" + str(self.error_line) if self.error_line else ""
174
+ }</div>
175
+ <div class="exc-msg">{html.escape(self.error_message)}</div>
176
+ </div>
177
+
178
+ {
179
+ f'<div class="code-context">{context_html}</div>' if context_html else ""
180
+ }
181
+
182
+ {traceback_html}
183
+ </div>
184
+ <!-- Standard PyWire Client Script for Hot Reload -->
185
+ <script src="/_pywire/static/pywire.dev.min.js"></script>
186
+ </body>
187
+ </html>
188
+ """
189
+ return HTMLResponse(content)
190
+
191
+ async def handle_event(
192
+ self, handler_name: str, data: Dict[str, Any]
193
+ ) -> dict[str, Any]:
194
+ """No-op for error page."""
195
+ return await self.render_update(init=False)
@@ -0,0 +1,203 @@
1
+ import linecache
2
+ import os
3
+ import traceback
4
+ import urllib.parse
5
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional
6
+
7
+ if TYPE_CHECKING:
8
+ from types import TracebackType
9
+
10
+ from starlette.responses import HTMLResponse
11
+ from starlette.types import ASGIApp, Receive, Scope, Send
12
+
13
+ from pywire.compiler.exceptions import PyWireSyntaxError
14
+
15
+
16
+ class DevErrorMiddleware:
17
+ """
18
+ Middleware to catch exceptions and render a helpful debug page.
19
+ Active only in development mode.
20
+ """
21
+
22
+ def __init__(self, app: ASGIApp):
23
+ self.app = app
24
+
25
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
26
+ if scope["type"] != "http":
27
+ await self.app(scope, receive, send)
28
+ return
29
+
30
+ try:
31
+ await self.app(scope, receive, send)
32
+ except Exception as exc:
33
+ # Check if headers already sent? Starlette handles this if we return Response?
34
+ # If we are midway through streaming, we might be in trouble, but for now catch all.
35
+ response = self.render_error_page(exc)
36
+ await response(scope, receive, send)
37
+
38
+ def __getattr__(self, name: str) -> Any:
39
+ return getattr(self.app, name)
40
+
41
+ def render_error_page(self, exc: Exception) -> HTMLResponse:
42
+ # Check if this is a compile-time error
43
+ if isinstance(exc, PyWireSyntaxError):
44
+ return self._render_compile_error(exc)
45
+
46
+ exc_type = type(exc).__name__
47
+ exc_msg = str(exc)
48
+
49
+ # Get traceback
50
+ tb = exc.__traceback__
51
+
52
+ # Skip top-level framework frames to focus on user code/relevant calls
53
+ # (optional refinement)
54
+
55
+ frames = self._get_frames(tb)
56
+ is_framework_error = (
57
+ self._is_framework_error(frames[-1]["filename"]) if frames else False
58
+ )
59
+
60
+ html_content = self._generate_html(
61
+ exc_type, exc_msg, frames, is_framework_error
62
+ )
63
+ return HTMLResponse(html_content, status_code=500)
64
+
65
+ def _render_compile_error(self, exc: PyWireSyntaxError) -> HTMLResponse:
66
+ """Render a specialized error page for compile-time syntax errors."""
67
+ from pywire.runtime.error_renderer import render_template
68
+
69
+ # Script URL logic for 500 pages (usually dev mode)
70
+ script_url = "/_pywire/static/pywire.dev.min.js"
71
+ if hasattr(self.app, "state") and hasattr(self.app.state, "pywire"):
72
+ script_url = self.app.state.pywire._get_client_script_url()
73
+
74
+ # Context lines logic reused...
75
+ # (Alternatively, could we reuse CompileErrorPage?
76
+ # No, DevErrorMiddleware catches exceptions, CompileErrorPage is a page.
77
+ # But logic is identical. For now, copying logic for simplicity
78
+ # as CompileErrorPage might have different lifecycle).
79
+
80
+ context_lines_data = []
81
+ if exc.file_path and exc.line and os.path.exists(exc.file_path):
82
+ try:
83
+ linecache.checkcache(exc.file_path)
84
+ lines = linecache.getlines(exc.file_path)
85
+ start = max(1, exc.line - 5)
86
+ end = min(len(lines), exc.line + 5)
87
+
88
+ for i in range(start, end + 1):
89
+ if i <= len(lines):
90
+ context_lines_data.append(
91
+ {
92
+ "num": i,
93
+ "content": lines[i - 1].rstrip(),
94
+ "is_current": i == exc.line,
95
+ }
96
+ )
97
+ except Exception:
98
+ pass
99
+
100
+ short_path = (
101
+ self._shorten_path(exc.file_path) if exc.file_path else "unknown file"
102
+ )
103
+
104
+ html_content = render_template(
105
+ "error/compile_error.html",
106
+ {
107
+ "file_display": short_path,
108
+ "error_line": exc.line,
109
+ "error_message": exc.message,
110
+ "context_lines": context_lines_data,
111
+ "script_url": script_url,
112
+ "title": "PyWire Syntax Error",
113
+ },
114
+ )
115
+ return HTMLResponse(html_content, status_code=500)
116
+
117
+ def _get_frames(self, tb: Optional["TracebackType"]) -> List[Dict[str, Any]]:
118
+ frames = []
119
+ for frame, lineno in traceback.walk_tb(tb):
120
+ filename = frame.f_code.co_filename
121
+ func_name = frame.f_code.co_name
122
+ context = []
123
+
124
+ # Simple context reading for frames
125
+ try:
126
+ if os.path.exists(filename):
127
+ # linecache handles reading
128
+ start = max(1, lineno - 5)
129
+ end = lineno + 5
130
+ lines = linecache.getlines(filename) # Will return [] if fails?
131
+ if lines:
132
+ for i in range(start, end + 1):
133
+ if i <= len(lines):
134
+ context.append(
135
+ {
136
+ "num": i,
137
+ "content": lines[i - 1].rstrip(),
138
+ "is_current": i == lineno,
139
+ }
140
+ )
141
+ except Exception:
142
+ pass
143
+
144
+ frames.append(
145
+ {
146
+ "filename": filename,
147
+ "short_filename": self._shorten_path(filename),
148
+ "func_name": func_name,
149
+ "lineno": lineno,
150
+ "context": context,
151
+ "is_user_code": self._is_user_code(filename),
152
+ }
153
+ )
154
+ return frames
155
+
156
+ def _is_framework_error(self, filename: str) -> bool:
157
+ return "pywire/src/pywire" in filename or "site-packages/pywire" in filename
158
+
159
+ def _is_user_code(self, filename: str) -> bool:
160
+ return not self._is_framework_error(filename) and "<frozen" not in filename
161
+
162
+ def _shorten_path(self, path: str) -> str:
163
+ cwd = os.getcwd()
164
+ if path.startswith(cwd):
165
+ return os.path.relpath(path, cwd)
166
+ return path
167
+
168
+ def _generate_html(
169
+ self,
170
+ exc_type: str,
171
+ exc_msg: str,
172
+ frames: List[Dict[str, Any]],
173
+ is_framework_error: bool,
174
+ ) -> str:
175
+ from pywire.runtime.error_renderer import render_template
176
+
177
+ script_url = "/_pywire/static/pywire.dev.min.js"
178
+ if hasattr(self.app, "state") and hasattr(self.app.state, "pywire"):
179
+ script_url = self.app.state.pywire._get_client_script_url()
180
+
181
+ issue_title = urllib.parse.quote(f"Bug: {exc_type}: {exc_msg}")
182
+ issue_body = urllib.parse.quote(
183
+ f"### Description\nEncountered an error in PyWire.\n\n"
184
+ f"### Error\n`{exc_type}: {exc_msg}`\n\n### Traceback\n"
185
+ f"(Please paste relevant traceback here)"
186
+ )
187
+ github_url = f"https://github.com/pywire/pywire/issues/new?title={issue_title}&body={issue_body}"
188
+
189
+ return render_template(
190
+ "error/500.html",
191
+ {
192
+ "exc_type": exc_type,
193
+ "exc_msg": exc_msg,
194
+ "frames": frames,
195
+ "is_framework_error": is_framework_error,
196
+ "github_url": github_url,
197
+ "script_url": script_url,
198
+ "title": exc_type,
199
+ },
200
+ )
201
+
202
+
203
+ # import urllib.parse # Moved to top