11"""Command runner module."""
22
33# pylint: disable=R0903
4-
54from collections .abc import Callable
65from copy import deepcopy
76from dataclasses import asdict , is_dataclass
1514from openbb_core .app .extension_loader import ExtensionLoader
1615from openbb_core .app .model .abstract .error import OpenBBError
1716from openbb_core .app .model .abstract .warning import OpenBBWarning , cast_warning
17+ from openbb_core .app .model .extension import CachedAccessor
1818from openbb_core .app .model .metadata import Metadata
1919from openbb_core .app .model .obbject import OBBject
2020from openbb_core .app .provider_interface import ExtraParams
@@ -448,13 +448,13 @@ async def run(
448448 raise OpenBBError (e ) from e
449449 warn (str (e ), OpenBBWarning )
450450
451- try :
452- cls . _trigger_command_output_callbacks ( route , obbject )
453-
454- except Exception as e :
455- if Env ().DEBUG_MODE :
456- raise OpenBBError (e ) from e
457- warn (str (e ), OpenBBWarning )
451+ if isinstance ( obbject , OBBject ) :
452+ try :
453+ cls . _trigger_command_output_callbacks ( route , obbject )
454+ except Exception as e :
455+ if Env ().DEBUG_MODE :
456+ raise OpenBBError (e ) from e
457+ warn (str (e ), OpenBBWarning )
458458
459459 return obbject
460460
@@ -463,7 +463,8 @@ def _trigger_command_output_callbacks(cls, route: str, obbject: OBBject) -> None
463463 """Trigger command output callbacks for extensions."""
464464 loader = ExtensionLoader ()
465465 callbacks = loader .on_command_output_callbacks
466- results_only = False
466+ if not callbacks :
467+ return
467468
468469 # For each extension registered for all routes or the specific route,
469470 # we call its accessor on the OBBject.
@@ -473,53 +474,93 @@ def _trigger_command_output_callbacks(cls, route: str, obbject: OBBject) -> None
473474 # mutates the OBBject so we can pass this information to the interface.
474475 # We also set the _results_only attribute to True if any extension
475476 # indicates that only results should be returned.
476- if "*" in callbacks :
477- for ext in callbacks ["*" ]:
478- if ext .results_only is True :
479- results_only = True
480- if ext .immutable is True :
481- if hasattr (obbject , ext .name ):
482- obbject_copy = deepcopy (obbject )
483- accessor = getattr (obbject_copy , ext .name )
484- if iscoroutinefunction (accessor ):
485- run_async (accessor )
486- elif callable (accessor ):
487- accessor ()
488- elif ext .immutable is False :
489- if ext .results_only is True :
490- results_only = True
491- if hasattr (obbject , ext .name ):
492- accessor = getattr (obbject , ext .name )
493- if iscoroutinefunction (accessor ):
494- run_async (accessor )
495- elif callable (accessor ):
496- accessor ()
497- setattr (obbject , "_extension_modified" , True )
498-
499- if route in callbacks :
500- for ext in callbacks [route ]:
477+ results_only = False
478+ executed_keys : set [str ] = set ()
479+ ordered_extensions : list = []
480+ all_on_command_output_exts : list = []
481+
482+ def _extension_key (ext ) -> str :
483+ if key := getattr (ext , "identifier" , None ):
484+ return str (key )
485+ if path := getattr (ext , "import_path" , None ):
486+ return f"{ path } :{ getattr (ext , 'name' , id (ext ))} "
487+ return str (getattr (ext , "name" , id (ext )))
488+
489+ def _clone_for_immutable (source : OBBject ) -> OBBject | None :
490+ try :
491+ new_source = source .model_copy ()
492+ new_source = OBBject .model_validate (source .model_dump ())
493+ return source .model_validate (new_source )
494+ except Exception as e :
495+ warn (
496+ "Skipped immutable callback because the OBBject "
497+ f"could not be duplicated. { e } " ,
498+ OpenBBWarning ,
499+ )
500+ return None
501+
502+ for ext_list in callbacks .values ():
503+ all_on_command_output_exts .extend (ext_list )
504+
505+ for ext in callbacks .get ("*" , []):
506+ key = _extension_key (ext )
507+ if key not in executed_keys :
508+ executed_keys .add (key )
509+ ordered_extensions .append (ext )
510+
511+ for ext in callbacks .get (route , []):
512+ key = _extension_key (ext )
513+ if key not in executed_keys :
514+ executed_keys .add (key )
515+ ordered_extensions .append (ext )
516+
517+ try :
518+ for ext in ordered_extensions :
501519 if ext .results_only is True :
502520 results_only = True
503521
504- if ext .immutable is True :
505- if hasattr (obbject , ext .name ):
506- obbject_copy = deepcopy (obbject )
507- accessor = getattr (obbject_copy , ext .name )
508- if iscoroutinefunction (accessor ):
509- run_async (accessor )
510- elif callable (accessor ):
511- accessor ()
512- elif ext .immutable is False and hasattr (obbject , ext .name ):
513- accessor = getattr (obbject , ext .name )
514- if iscoroutinefunction (accessor ):
515- run_async (accessor )
516- elif callable (accessor ):
517- accessor ()
518- setattr (obbject , "_extension_modified" , True )
519-
520- if results_only is True :
521- setattr (obbject , "_results_only" , True )
522- setattr (obbject , "_extension_modified" , True )
522+ if ext .command_output_paths and route not in ext .command_output_paths :
523+ continue
524+
525+ accessors = getattr (type (obbject ), "accessors" , set ())
526+ if ext .name not in accessors :
527+ continue
528+
529+ descriptor = type (obbject ).__dict__ .get (ext .name )
530+ if not isinstance (descriptor , CachedAccessor ):
531+ continue
532+
533+ factory = descriptor ._accessor # type: ignore[attr-defined]
534+
535+ target = _clone_for_immutable (obbject ) if ext .immutable else obbject
536+
537+ if target is None :
538+ continue
539+
540+ if iscoroutinefunction (factory ):
541+ run_async (factory , target )
542+ else :
543+ result = factory (target )
544+ if callable (result ):
545+ result ()
546+
547+ if ext .immutable is False :
548+ object .__setattr__ (obbject , "_extension_modified" , True )
549+
550+ if results_only is True :
551+ object .__setattr__ (obbject , "_results_only" , True )
552+ object .__setattr__ (obbject , "_extension_modified" , True )
553+
554+ except Exception as e :
555+ raise OpenBBError (e ) from e
556+
557+ for ext in all_on_command_output_exts :
558+ if ext .name in type (obbject ).__dict__ :
559+ object .__setattr__ (
560+ obbject ,
561+ ext .name ,
562+ "Accessor is not callable outside of function execution." ,
563+ )
523564
524565
525566class CommandRunner :
0 commit comments