@@ -386,8 +386,37 @@ def _prep_component_kwargs(
386386 callable_info : CallableInfo ,
387387 attrs : AttributesDict ,
388388 children : Template ,
389+ provided_attrs : tuple [Attribute , ...] = (),
390+ raise_on_requires_positional = True ,
391+ raise_on_missing = True ,
389392) -> AttributesDict :
390- if callable_info .requires_positional :
393+ """
394+ Matchup kwargs from multiple sources to target the given callable.
395+
396+ `provided_attrs`:
397+ These can be used by extensions that want to provide
398+ attrs even if they are not specified in the component's `attrs` in
399+ the template. If an attribute with the same name is provided in
400+ `attrs` then it takes priority over entries in `provided_attrs`.
401+ @NOTE: These will be injected into any component with `**kwargs`
402+ in their signature unless provided already by `attrs`.
403+
404+ `raise_on_requires_positional`:
405+ Optionally check and raise `TypeError` if the `callable_info` requires
406+ positional arguments which we cannot fulfill normally.
407+ An exception might not be desired if the caller will finish preparing
408+ the arguments after this call.
409+
410+ `raise_on_missing`:
411+ Optionally check and raise `TypeError` if we are not able to fulfill all
412+ the arguments the `callable_info` expects since in the common case this
413+ raise an exception whose cause might not be clear.
414+ An exception might not be desired if the caller will finish preparing
415+ the arguments after this call.
416+ """
417+
418+ # We can't know what kwarg to put here...
419+ if raise_on_requires_positional and callable_info .requires_positional :
391420 raise TypeError (
392421 "Component callables cannot have required positional arguments."
393422 )
@@ -403,12 +432,20 @@ def _prep_component_kwargs(
403432 if "children" in callable_info .named_params or callable_info .kwargs :
404433 kwargs ["children" ] = children
405434
435+ # Add in provided attrs if they haven't been set already and are wanted.
436+ for pattr_name , pattr_value in provided_attrs :
437+ if pattr_name not in kwargs and (
438+ pattr_name in callable_info .named_params or callable_info .kwargs
439+ ):
440+ kwargs [pattr_name ] = pattr_value
441+
406442 # Check to make sure we've fully satisfied the callable's requirements
407- missing = callable_info .required_named_params - kwargs .keys ()
408- if missing :
409- raise TypeError (
410- f"Missing required parameters for component: { ', ' .join (missing )} "
411- )
443+ if raise_on_missing :
444+ missing = callable_info .required_named_params - kwargs .keys ()
445+ if missing :
446+ raise TypeError (
447+ f"Missing required parameters for component: { ', ' .join (missing )} "
448+ )
412449
413450 return kwargs
414451
@@ -497,6 +534,90 @@ def to_tnode(self, template: Template) -> TNode:
497534 return self ._to_tnode (CachableTemplate (template ))
498535
499536
537+ class IComponentProcessor (t .Protocol ):
538+ """Isolate component processing to allow for replacement."""
539+
540+ def process (
541+ self ,
542+ template : Template ,
543+ last_ctx : ProcessContext ,
544+ component_callable : t .Annotated [object , ComponentCallable ],
545+ attrs : tuple [TAttribute , ...],
546+ component_template : Template ,
547+ provided_attrs : tuple [Attribute , ...] = (),
548+ ) -> Template :
549+ """
550+ Process available component details into a Template.
551+ """
552+ ...
553+
554+
555+ class ComponentProcessor (IComponentProcessor ):
556+ """
557+ Default component processor.
558+ """
559+
560+ def process (
561+ self ,
562+ template : Template ,
563+ last_ctx : ProcessContext ,
564+ component_callable : t .Annotated [object , ComponentCallable ],
565+ attrs : tuple [TAttribute , ...],
566+ component_template : Template ,
567+ provided_attrs : tuple [Attribute , ...] = (),
568+ ) -> Template :
569+ """
570+ Process available component details into a Template.
571+
572+ There are two general "styles" supported:
573+
574+ 1. FunctionComponent
575+
576+ Calling `component_callable` with the prepared kwargs should
577+ return a `Template`.
578+
579+ The primary purpose of this style is to support
580+ using a normal function as a component.
581+
582+ 2. FactoryComponent
583+
584+ Calling `component_callable` with the prepared kwargs should
585+ return another `Callable` which when called with no arguments should
586+ return a `Template`.
587+
588+ The primary purpose of this style is to support
589+ using a `dataclass` with `def __call__(self) -> Template` as a
590+ component.
591+ """
592+ if not callable (component_callable ):
593+ raise TypeError (
594+ f"Component callable must be callable: { type (component_callable )} "
595+ )
596+ kwargs = _prep_component_kwargs (
597+ get_callable_info (component_callable ),
598+ _resolve_t_attrs (attrs , template .interpolations ),
599+ children = component_template ,
600+ provided_attrs = provided_attrs ,
601+ raise_on_requires_positional = True ,
602+ raise_on_missing = True ,
603+ )
604+ res1 = component_callable (** kwargs ) # ty: ignore[call-top-callable]
605+ if isinstance (res1 , Template ):
606+ return res1
607+ elif callable (res1 ):
608+ res2 = res1 () # ty: ignore[call-top-callable]
609+ if isinstance (res2 , Template ):
610+ return res2
611+ else :
612+ raise TypeError (
613+ f"Component object must return Template when called: { type (res2 )} "
614+ )
615+ else :
616+ raise TypeError (
617+ f"Component callable must return Template or Callable: { type (res1 )} "
618+ )
619+
620+
500621class ITemplateProcessor (t .Protocol ):
501622 def process (self , root_template : Template , assume_ctx : ProcessContext ) -> str : ...
502623
@@ -505,6 +626,10 @@ def process(self, root_template: Template, assume_ctx: ProcessContext) -> str: .
505626class TemplateProcessor (ITemplateProcessor ):
506627 parser_api : ITemplateParserProxy = field (default_factory = CachedTemplateParserProxy )
507628
629+ component_processor_api : IComponentProcessor = field (
630+ default_factory = ComponentProcessor
631+ )
632+
508633 escape_html_text : Callable = default_escape_html_text
509634
510635 escape_html_comment : Callable = default_escape_html_comment
@@ -668,60 +793,52 @@ def _process_attrs(
668793 return attrs_str
669794 return ""
670795
671- def _process_component (
796+ def _extract_component_template (
672797 self ,
673798 template : Template ,
674- last_ctx : ProcessContext ,
675799 attrs : tuple [TAttribute , ...],
676800 start_i_index : int ,
677801 end_i_index : int | None ,
678- ) -> str :
679- """
680- Invoke a component and process the result into a string.
681- """
802+ check_callables : bool = True ,
803+ ) -> Template :
682804 body_start_s_index = (
683805 start_i_index
684806 + 1
685807 + len ([1 for attr in attrs if not isinstance (attr , TLiteralAttribute )])
686808 )
687- start_i = template .interpolations [start_i_index ]
688- component_callable = t .cast (ComponentCallable , start_i .value )
689809 if start_i_index != end_i_index and end_i_index is not None :
690810 # @TODO: We should do this during parsing.
691- children_template = extract_embedded_template (
692- template , body_start_s_index , end_i_index
693- )
694- if component_callable != template .interpolations [end_i_index ].value :
811+ if (
812+ check_callables
813+ and template .interpolations [start_i_index ].value
814+ != template .interpolations [end_i_index ].value
815+ ):
695816 raise TypeError (
696817 "Component callable in start tag must match component callable in end tag."
697818 )
819+ return extract_embedded_template (template , body_start_s_index , end_i_index )
698820 else :
699- children_template = t ""
821+ return t ""
700822
701- if not callable (component_callable ):
702- raise TypeError ("Component callable must be callable." )
703-
704- kwargs = _prep_component_kwargs (
705- get_callable_info (component_callable ),
706- _resolve_t_attrs (attrs , template .interpolations ),
707- children = children_template ,
823+ def _process_component (
824+ self ,
825+ template : Template ,
826+ last_ctx : ProcessContext ,
827+ attrs : tuple [TAttribute , ...],
828+ start_i_index : int ,
829+ end_i_index : int | None ,
830+ ) -> str :
831+ """
832+ Invoke a component and process the result into a string.
833+ """
834+ children_template = self ._extract_component_template (
835+ template , attrs , start_i_index , end_i_index , check_callables = True
708836 )
709-
710- result_t = component_callable (** kwargs )
711- if (
712- result_t is not None
713- and not isinstance (result_t , Template )
714- and callable (result_t )
715- ):
716- component_obj = t .cast (ComponentObject , result_t ) # ty: ignore[redundant-cast]
717- result_t = component_obj ()
718- else :
719- component_obj = None
720-
721- if isinstance (result_t , Template ):
722- return self ._process_template (result_t , last_ctx )
723- else :
724- raise TypeError (f"Unknown component return value: { type (result_t )} " )
837+ component_callable = template .interpolations [start_i_index ].value
838+ result_t = self .component_processor_api .process (
839+ template , last_ctx , component_callable , attrs , children_template
840+ )
841+ return self ._process_template (result_t , last_ctx )
725842
726843 def _process_raw_texts (
727844 self ,
0 commit comments