11import inspect
22import logging
3- import textwrap
43import typing as t
54import unittest
65import uuid
@@ -148,9 +147,10 @@ class Plan(BaseConsoleEvent):
148147@dataclass (kw_only = True )
149148class LogTestResults (BaseConsoleEvent ):
150149 result : unittest .result .TestResult
151- output : str | None
150+ output : str | None = None
152151 target_dialect : str
153152
153+
154154@dataclass (kw_only = True )
155155class ShowSQL (BaseConsoleEvent ):
156156 sql : str
@@ -221,7 +221,7 @@ class ShowTableDiffSummary(BaseConsoleEvent):
221221
222222@dataclass (kw_only = True )
223223class PlanBuilt (BaseConsoleEvent ):
224- plan : SQLMeshPlan
224+ plan : SQLMeshPlan
225225
226226ConsoleEvent = (
227227 StartPlanEvaluation
@@ -277,6 +277,8 @@ class PlanBuilt(BaseConsoleEvent):
277277]
278278
279279T = t .TypeVar ("T" )
280+ EventType = t .TypeVar ("EventType" , bound = BaseConsoleEvent )
281+
280282
281283def get_console_event_by_name (
282284 event_name : str ,
@@ -303,7 +305,7 @@ def __init_subclass__(cls):
303305 for known_event in known_events_classes :
304306 assert inspect .isclass (known_event ), "event must be a class"
305307 known_events .append (known_event .__name__ )
306-
308+
307309
308310 # Iterate through all the available abstract methods in console
309311 for method_name in Console .__abstractmethods__ :
@@ -319,7 +321,7 @@ def __init_subclass__(cls):
319321 # events has it's values checked. The dataclass should define the
320322 # required fields and everything else should be sent to a catchall
321323 # argument in the dataclass for the event
322-
324+
323325 # Convert method name from snake_case to camel case
324326 camel_case_method_name = "" .join (
325327 word .capitalize ()
@@ -329,7 +331,9 @@ def __init_subclass__(cls):
329331 if camel_case_method_name in known_events :
330332 logger .debug (f"Creating { method_name } for { camel_case_method_name } " )
331333 signature = inspect .signature (getattr (Console , method_name ))
332- handler = cls .create_event_handler (method_name , camel_case_method_name , signature )
334+ event_cls = get_console_event_by_name (camel_case_method_name )
335+ assert event_cls is not None , f"Event { camel_case_method_name } not found"
336+ handler = cls .create_event_handler (method_name , event_cls , signature )
333337 setattr (cls , method_name , handler )
334338 else :
335339 logger .debug (f"Creating { method_name } for unknown event" )
@@ -338,51 +342,23 @@ def __init_subclass__(cls):
338342 setattr (cls , method_name , handler )
339343
340344 @classmethod
341- def create_event_handler (cls , method_name : str , event_name : str , signature : inspect .Signature ):
342- func_signature , call_params = cls .create_signatures_and_params (signature )
345+ def create_event_handler (cls , method_name : str , event_cls : type [BaseConsoleEvent ], signature : inspect .Signature ) -> t .Callable [..., None ]:
346+ """Create a GeneratedCallable for known events."""
347+ def handler (self : IntrospectingConsole , * args : t .Any , ** kwargs : t .Any ) -> None :
348+ callable_handler = GeneratedCallable (self , event_cls , signature , method_name )
349+ return callable_handler (* args , ** kwargs )
343350
344- event_handler_str = textwrap .dedent (f"""
345- def { method_name } ({ ", " .join (func_signature )} ):
346- self.publish_known_event('{ event_name } ', { ", " .join (call_params )} )
347- """ )
348- exec (event_handler_str )
349- return t .cast (t .Callable [[t .Any ], t .Any ], locals ()[method_name ])
351+ return handler
350352
351- @classmethod
352- def create_signatures_and_params (cls , signature : inspect .Signature ):
353- func_signature : list [str ] = []
354- call_params : list [str ] = []
355- for param_name , param in signature .parameters .items ():
356- if param_name == "self" :
357- func_signature .append ("self" )
358- continue
359-
360- if param .default is inspect ._empty :
361- param_type_name = param .annotation
362- if not isinstance (param_type_name , str ):
363- param_type_name = param_type_name .__name__
364- func_signature .append (f"{ param_name } : '{ param_type_name } '" )
365- else :
366- default_value = param .default
367- param_type_name = param .annotation
368- if not isinstance (param_type_name , str ):
369- param_type_name = param_type_name .__name__
370- if isinstance (param .default , str ):
371- default_value = f"'{ param .default } '"
372- func_signature .append (f"{ param_name } : '{ param_type_name } ' = { default_value } " )
373- call_params .append (f"{ param_name } ={ param_name } " )
374- return (func_signature , call_params )
375353
376354 @classmethod
377- def create_unknown_event_handler (cls , method_name : str , signature : inspect .Signature ):
378- func_signature , call_params = cls .create_signatures_and_params (signature )
355+ def create_unknown_event_handler (cls , method_name : str , signature : inspect .Signature ) -> t .Callable [..., None ]:
356+ """Create an UnknownEventCallable for unknown events."""
357+ def handler (self : IntrospectingConsole , * args : t .Any , ** kwargs : t .Any ) -> None :
358+ callable_handler = UnknownEventCallable (self , method_name , signature )
359+ return callable_handler (* args , ** kwargs )
379360
380- event_handler_str = textwrap .dedent (f"""
381- def { method_name } ({ ", " .join (func_signature )} ):
382- self.publish_unknown_event('{ method_name } ', { ", " .join (call_params )} )
383- """ )
384- exec (event_handler_str )
385- return t .cast (t .Callable [[t .Any ], t .Any ], locals ()[method_name ])
361+ return handler
386362
387363 def __init__ (self , log_override : logging .Logger | None = None ) -> None :
388364 self ._handlers : dict [str , ConsoleEventHandler ] = {}
@@ -391,23 +367,6 @@ def __init__(self, log_override: logging.Logger | None = None) -> None:
391367 self .logger .debug (f"EventConsole[{ self .id } ]: created" )
392368 self .categorizer = None
393369
394- def publish_known_event (self , event_name : str , ** kwargs : t .Any ) -> None :
395- console_event = get_console_event_by_name (event_name )
396- assert console_event is not None , f"Event { event_name } not found"
397-
398- expected_kwargs_fields = console_event .__dataclass_fields__
399- expected_kwargs : dict [str , t .Any ] = {}
400- unknown_args : dict [str , t .Any ] = {}
401- for key , value in kwargs .items ():
402- if key not in expected_kwargs_fields :
403- unknown_args [key ] = value
404- else :
405- expected_kwargs [key ] = value
406-
407- event = console_event (** expected_kwargs , unknown_args = unknown_args )
408-
409- self .publish (event )
410-
411370 def publish (self , event : ConsoleEvent ) -> None :
412371 self .logger .debug (
413372 f"EventConsole[{ self .id } ]: sending event { event .__class__ .__name__ } to { len (self ._handlers )} "
@@ -446,6 +405,90 @@ def capture_built_plan(self, plan: SQLMeshPlan) -> None:
446405 """Capture the built plan and publish a PlanBuilt event."""
447406 self .publish (PlanBuilt (plan = plan ))
448407
408+
409+ class GeneratedCallable (t .Generic [EventType ]):
410+ """A callable that dynamically handles console method invocations and converts them to events."""
411+
412+ def __init__ (
413+ self ,
414+ console : IntrospectingConsole ,
415+ event_cls : type [EventType ],
416+ original_signature : inspect .Signature ,
417+ method_name : str
418+ ):
419+ self .console = console
420+ self .event_cls = event_cls
421+ self .original_signature = original_signature
422+ self .method_name = method_name
423+
424+ def __call__ (self , * args : t .Any , ** kwargs : t .Any ) -> None :
425+ """Create an instance of the event class with the provided arguments."""
426+
427+ # Bind arguments to the original signature
428+ try :
429+ bound = self .original_signature .bind (self .console , * args , ** kwargs )
430+ bound .apply_defaults ()
431+ except TypeError as e :
432+ # If binding fails, collect all args/kwargs as unknown
433+ self .console .logger .warning (f"Failed to bind arguments for { self .method_name } : { e } " )
434+ unknown_args = {str (i ): arg for i , arg in enumerate (args [1 :])} # Skip 'self'
435+ unknown_args .update (kwargs )
436+ self ._create_and_publish_event ({})
437+ return
438+
439+ # Process bound arguments
440+ bound_args = dict (bound .arguments )
441+ bound_args .pop ("self" , None ) # Remove self from arguments
442+
443+ self ._create_and_publish_event (bound_args )
444+
445+ def _create_and_publish_event (self , bound_args : dict [str , t .Any ]) -> None :
446+ """Create and publish the event with proper argument handling."""
447+ expected_fields = self .event_cls .__dataclass_fields__
448+ expected_kwargs : dict [str , t .Any ] = {}
449+ unknown_args : dict [str , t .Any ] = {}
450+
451+ # Process bound arguments
452+ for key , value in bound_args .items ():
453+ if key in expected_fields :
454+ expected_kwargs [key ] = value
455+ else :
456+ unknown_args [key ] = value
457+
458+ # Create and publish the event
459+ event = self .event_cls (** expected_kwargs )
460+ self .console .publish (t .cast (ConsoleEvent , event ))
461+
462+
463+ class UnknownEventCallable :
464+ """A callable for handling unknown console events."""
465+
466+ def __init__ (
467+ self ,
468+ console : IntrospectingConsole ,
469+ method_name : str ,
470+ original_signature : inspect .Signature
471+ ):
472+ self .console = console
473+ self .method_name = method_name
474+ self .original_signature = original_signature
475+
476+ def __call__ (self , * args : t .Any , ** kwargs : t .Any ) -> None :
477+ """Handle unknown event method calls."""
478+ # Bind arguments to the original signature
479+ try :
480+ bound = self .original_signature .bind (* args , ** kwargs )
481+ bound .apply_defaults ()
482+ bound_args = dict (bound .arguments )
483+ bound_args .pop ("self" , None ) # Remove self from arguments
484+ except TypeError :
485+ # If binding fails, collect all args/kwargs
486+ bound_args = {str (i ): arg for i , arg in enumerate (args [1 :])} # Skip 'self'
487+ bound_args .update (kwargs )
488+
489+ self .console .publish_unknown_event (self .method_name , ** bound_args )
490+
491+
449492class EventConsole (IntrospectingConsole ):
450493 """
451494 A console implementation that manages and publishes events related to
0 commit comments