11from __future__ import annotations
22
3+ import asyncio
34from contextvars import copy_context , ContextVar
45from typing import TYPE_CHECKING , Any , Callable , Dict
56import sys
89import inspect
910import pkgutil
1011import time
11- import traceback
1212from importlib .util import spec_from_file_location
1313import json
1414import os
15- import re
15+
16+ try :
17+ from fastapi import FastAPI , Request , Response , Body
18+ from fastapi .responses import JSONResponse
19+ from fastapi .staticfiles import StaticFiles
20+ from starlette .responses import Response as StarletteResponse
21+ from starlette .datastructures import MutableHeaders
22+ from starlette .types import ASGIApp , Scope , Receive , Send
23+ import uvicorn
24+ except ImportError :
25+ FastAPI = None
26+ Request = None
27+ Response = None
28+ Body = None
29+ JSONResponse = None
30+ StaticFiles = None
31+ StarletteResponse = None
32+ MutableHeaders = None
33+ ASGIApp = None
34+ Scope = None
35+ Receive = None
36+ Send = None
37+ uvicorn = None
1638
1739from dash .fingerprint import check_fingerprint
1840from dash import _validate
1941from dash .exceptions import PreventUpdate
2042from .base_server import BaseDashServer , RequestAdapter
21-
22- from fastapi import FastAPI , Request , Response , Body
23- from fastapi .responses import JSONResponse
24- from fastapi .staticfiles import StaticFiles
25- from starlette .responses import Response as StarletteResponse
26- from starlette .datastructures import MutableHeaders
27- from starlette .types import ASGIApp , Scope , Receive , Send
28- import uvicorn
43+ from ._utils import format_traceback_html
2944
3045if TYPE_CHECKING : # pragma: no cover - typing only
31- from dash . dash import Dash
46+ from dash import Dash
3247
3348
3449_current_request_var = ContextVar ("dash_current_request" , default = None )
@@ -49,7 +64,7 @@ def get_current_request() -> Request:
4964 return req
5065
5166
52- class CurrentRequestMiddleware :
67+ class CurrentRequestMiddleware : # pylint: disable=too-few-public-methods
5368 def __init__ (self , app : ASGIApp ) -> None : # type: ignore[name-defined]
5469 self .app = app
5570
@@ -66,23 +81,27 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: #
6681 finally :
6782 reset_current_request (token )
6883
84+
6985# Internal config helpers (local to this file)
7086_CONFIG_PATH = os .path .join (os .path .dirname (__file__ ), "dash_config.json" )
7187
88+
7289def _save_config (config ):
73- with open (_CONFIG_PATH , "w" ) as f :
90+ with open (_CONFIG_PATH , "w" , encoding = "utf-8" ) as f :
7491 json .dump (config , f )
7592
93+
7694def _load_config ():
7795 resp = {"debug" : False }
7896 try :
7997 if os .path .exists (_CONFIG_PATH ):
80- with open (_CONFIG_PATH , "r" ) as f :
98+ with open (_CONFIG_PATH , "r" , encoding = "utf-8" ) as f :
8199 resp = json .load (f )
82- except Exception :
100+ except ( json . JSONDecodeError , OSError ) :
83101 pass # ignore errors
84102 return resp
85103
104+
86105def _remove_config ():
87106 try :
88107 os .remove (_CONFIG_PATH )
@@ -130,113 +149,9 @@ def register_error_handlers(self):
130149 self .error_handling_mode = "ignore"
131150
132151 def _get_traceback (self , _secret , error : Exception ):
133- tb = error .__traceback__
134- errors = traceback .format_exception (type (error ), error , tb )
135- pass_errs = []
136- callback_handled = False
137- for err in errors :
138- if self .error_handling_mode == "prune" :
139- if not callback_handled :
140- if "callback invoked" in str (err ) and "_callback.py" in str (err ):
141- callback_handled = True
142- continue
143- pass_errs .append (err )
144- formatted_tb = "" .join (pass_errs )
145- error_type = type (error ).__name__
146- error_msg = str (error )
147-
148- # Parse traceback lines to group by file
149- file_cards = []
150- pattern = re .compile (r' File "(.+)", line (\d+), in (\w+)' )
151- lines = formatted_tb .split ("\n " )
152- current_file = None
153- card_lines = []
154-
155- for line in lines [:- 1 ]: # Skip the last line (error message)
156- match = pattern .match (line )
157- if match :
158- if current_file and card_lines :
159- file_cards .append ((current_file , card_lines ))
160- current_file = (
161- f"{ match .group (1 )} (line { match .group (2 )} , in { match .group (3 )} )"
162- )
163- card_lines = [line ]
164- elif current_file :
165- card_lines .append (line )
166- if current_file and card_lines :
167- file_cards .append ((current_file , card_lines ))
168-
169- cards_html = ""
170- for filename , card in file_cards :
171- cards_html += (
172- f"""
173- <div class="error-card">
174- <div class="error-card-header">{ filename } </div>
175- <pre class="error-card-traceback">"""
176- + "\n " .join (card )
177- + """</pre>
178- </div>
179- """
180- )
181-
182- html = f"""
183- <!doctype html>
184- <html lang="en">
185- <head>
186- <title>{ error_type } : { error_msg } // FastAPI Debugger</title>
187- <style>
188- body {{ font-family: monospace; background: #fff; color: #333; }}
189- .debugger {{ margin: 2em; max-width: 700px; }}
190- .error-card {{
191- border: 1px solid #ccc;
192- border-radius: 6px;
193- margin-bottom: 1em;
194- padding: 1em;
195- background: #f9f9f9;
196- box-shadow: 0 2px 4px rgba(0,0,0,0.03);
197- overflow: auto;
198- }}
199- .error-card-header {{
200- font-weight: bold;
201- margin-bottom: 0.5em;
202- color: #0074d9;
203- }}
204- .error-card-traceback {{
205- max-height: 150px;
206- overflow: auto;
207- margin: 0;
208- white-space: pre-wrap;
209- }}
210- .plain textarea {{ width: 100%; height: 10em; resize: vertical; overflow: auto; }}
211- h1 {{ color: #c00; }}
212- </style>
213- </head>
214- <body style="padding-bottom:10px">
215- <div class="debugger">
216- <h1>{ error_type } </h1>
217- <div class="detail">
218- <p class="errormsg">{ error_type } : { error_msg } </p>
219- </div>
220- <h2 class="traceback">Traceback <em>(most recent call last)</em></h2>
221- { cards_html }
222- <blockquote>{ error_type } : { error_msg } </blockquote>
223- <div class="plain">
224- <p>This is the Copy/Paste friendly version of the traceback.</p>
225- <textarea readonly>{ formatted_tb } </textarea>
226- </div>
227- <div class="explanation">
228- The debugger caught an exception in your ASGI application. You can now
229- look at the traceback which led to the error.
230- </div>
231- <div class="footer">
232- Brought to you by <strong class="arthur">DON'T PANIC</strong>, your
233- friendly FastAPI powered traceback interpreter.
234- </div>
235- </div>
236- </body>
237- </html>
238- """
239- return html
152+ return format_traceback_html (
153+ error , self .error_handling_mode , "FastAPI Debugger" , "FastAPI"
154+ )
240155
241156 def register_prune_error_handler (self , _secret , prune_errors ):
242157 if prune_errors :
@@ -253,7 +168,7 @@ async def wrapped(*_args, **_kwargs):
253168 return wrapped
254169
255170 def setup_index (self , dash_app : Dash ):
256- async def index (request : Request ):
171+ async def index (_request : Request ):
257172 return Response (content = dash_app .index (), media_type = "text/html" )
258173
259174 # pylint: disable=protected-access
@@ -270,7 +185,7 @@ def _setup_catchall():
270185 ** _load_config (), first_run = False
271186 ) # do this to make sure dev tools are enabled
272187
273- async def catchall (request : Request ):
188+ async def catchall (_request : Request ):
274189 return Response (content = dash_app .index (), media_type = "text/html" )
275190
276191 # pylint: disable=protected-access
@@ -308,11 +223,10 @@ def after_request(self, func: Callable[[], Any] | None):
308223
309224 def run (self , dash_app : Dash , host , port , debug , ** kwargs ):
310225 frame = inspect .stack ()[2 ]
226+ dev_tools = dash_app ._dev_tools # pylint: disable=protected-access
311227 config = dict (
312228 {"debug" : debug } if debug else {"debug" : False },
313- ** {
314- f"dev_tools_{ k } " : v for k , v in dash_app ._dev_tools .items ()
315- }, # pylint: disable=protected-access
229+ ** {f"dev_tools_{ k } " : v for k , v in dev_tools .items ()},
316230 )
317231 _save_config (config )
318232 if debug :
@@ -348,22 +262,26 @@ def make_response(
348262 def jsonify (self , obj : Any ):
349263 return JSONResponse (content = obj )
350264
351- def _make_before_middleware (self , func : Callable [[], Any ] | None ):
265+ def _make_before_middleware (self , _func : Callable [[], Any ] | None ):
352266 async def middleware (request , call_next ):
353267 try :
354268 response = await call_next (request )
355269 return response
356270 except PreventUpdate :
357271 # No content, nothing to update
358272 return Response (status_code = 204 )
359- except Exception as e :
273+ except (Exception ) as e : # pylint: disable=broad-except
274+ # Handle exceptions based on error_handling_mode
360275 if self .error_handling_mode in ["raise" , "prune" ]:
361276 # Prune the traceback to remove internal Dash calls
362277 tb = self ._get_traceback (None , e )
363278 return Response (content = tb , media_type = "text/html" , status_code = 500 )
364279 return JSONResponse (
365280 status_code = 500 ,
366- content = {"error" : "InternalServerError" , "message" : "An internal server error occurred." },
281+ content = {
282+ "error" : "InternalServerError" ,
283+ "message" : "An internal server error occurred." ,
284+ },
367285 )
368286
369287 return middleware
@@ -417,27 +335,25 @@ async def serve(request: Request, package_name: str, fingerprinted_path: str):
417335 dash_app , package_name , fingerprinted_path , request
418336 )
419337
420- # pylint: disable=protected-access
421- dash_app ._add_url (
422- "_dash-component-suites/{package_name}/{fingerprinted_path:path}" ,
423- serve ,
424- )
338+ name = "_dash-component-suites/{package_name}/{fingerprinted_path:path}"
339+ dash_app ._add_url (name , serve ) # pylint: disable=protected-access
425340
426- # pylint: disable=unused-argument
427341 def dispatch (self , dash_app : Dash ):
428342 async def _dispatch (request : Request ):
429343 # pylint: disable=protected-access
430344 body = await request .json ()
431- g = dash_app ._initialize_context (body ) # pylint: disable=protected-access
345+ cb_ctx = dash_app ._initialize_context (
346+ body
347+ ) # pylint: disable=protected-access
432348 func = dash_app ._prepare_callback (
433- g , body
349+ cb_ctx , body
434350 ) # pylint: disable=protected-access
435351 args = dash_app ._inputs_to_vals (
436- g .inputs_list + g .states_list
352+ cb_ctx .inputs_list + cb_ctx .states_list
437353 ) # pylint: disable=protected-access
438354 ctx = copy_context ()
439355 partial_func = dash_app ._execute_callback (
440- func , args , g .outputs_list , g
356+ func , args , cb_ctx .outputs_list , cb_ctx
441357 ) # pylint: disable=protected-access
442358 response_data = ctx .run (partial_func )
443359 if inspect .iscoroutine (response_data ):
@@ -494,20 +410,24 @@ def register_callback_api_routes(
494410 sig = inspect .signature (handler )
495411 param_names = list (sig .parameters .keys ())
496412
497- async def view_func (request : Request , body : dict = Body (...)):
498- # Only pass expected params; ignore extras
499- kwargs = {
500- k : v for k , v in body .items () if k in param_names and v is not None
501- }
502- if inspect .iscoroutinefunction (handler ):
503- result = await handler (** kwargs )
504- else :
505- result = handler (** kwargs )
506- return JSONResponse (content = result )
413+ def make_view_func (handler , param_names ):
414+ async def view_func (_request : Request , body : dict = Body (...)):
415+ kwargs = {
416+ k : v
417+ for k , v in body .items ()
418+ if k in param_names and v is not None
419+ }
420+ if inspect .iscoroutinefunction (handler ):
421+ result = await handler (** kwargs )
422+ else :
423+ result = handler (** kwargs )
424+ return JSONResponse (content = result )
425+
426+ return view_func
507427
508428 self .server .add_api_route (
509429 route ,
510- view_func ,
430+ make_view_func ( handler , param_names ) ,
511431 methods = methods ,
512432 name = endpoint ,
513433 include_in_schema = True ,
@@ -566,5 +486,5 @@ def origin(self):
566486 def path (self ):
567487 return self ._request .url .path
568488
569- async def get_json (self ): # async method retained
570- return await self ._request .json ()
489+ def get_json (self ):
490+ return asyncio . run ( self ._request .json () )
0 commit comments