@@ -484,13 +484,15 @@ class YoTask:
484
484
script : str
485
485
dependencies : t .List [str ]
486
486
conflicts : t .List [str ]
487
+ prereq_for : t .List [str ]
487
488
488
489
@classmethod
489
490
def create_from_string (
490
491
cls , name : str , script : str , path : str = "(memory)"
491
492
) -> "YoTask" :
492
493
dependencies = []
493
494
conflicts = []
495
+ prereq_for = []
494
496
all_tasks = list_tasks ()
495
497
lines = script .split ("\n " )
496
498
for i in range (len (lines )):
@@ -509,7 +511,11 @@ def create_from_string(
509
511
"MAYBE_DEPENDS_ON" , "# MAYBE_DEPENDS_ON"
510
512
)
511
513
lines [i ] = line
512
- return YoTask (name , path , "\n " .join (lines ), dependencies , conflicts )
514
+ elif line .startswith ("PREREQ_FOR" ):
515
+ prereq_for .append (line .split (None , maxsplit = 1 )[1 ])
516
+ return YoTask (
517
+ name , path , "\n " .join (lines ), dependencies , conflicts , prereq_for
518
+ )
513
519
514
520
@classmethod
515
521
@lru_cache (maxsize = None )
@@ -532,6 +538,10 @@ def load(cls, name: str) -> "YoTask":
532
538
script = f .read ()
533
539
return cls .create_from_string (name , script , path = path )
534
540
541
+ def insert_prereq (self , other : str ) -> None :
542
+ self .dependencies .append (other )
543
+ self .script = f"DEPENDS_ON { other } \n { self .script } "
544
+
535
545
536
546
@lru_cache (maxsize = 1 )
537
547
def list_tasks () -> t .List [str ]:
@@ -607,51 +617,67 @@ def _task_run(ctx: YoCtx, inst: YoInstance, task: YoTask) -> None:
607
617
def run_all_tasks (
608
618
ctx : YoCtx , inst : YoInstance , tasks : t .Iterable [t .Union [YoTask , str ]]
609
619
) -> None :
610
- # Tasks can have dependencies. We need to be sure to load the full set of
611
- # dependencies in the list of tasks we're given. We also need to verify
612
- # there are no circular dependencies. And it's nice to launch them in order
613
- # that will satisfy their dependencies, but the "DEPENDS_ON" function should
614
- # successfully handle waiting until all depnedencies are completed anyway.
615
-
616
- # 0: not visited, 1: visiting, 2: finished visit
620
+ # The caller may specify tasks as either strings or YoTask instances, for
621
+ # convenience. Let's get everything into a "name_to_task" dict.
622
+ name_to_task : t .Dict [str , YoTask ] = {}
623
+ for task in tasks :
624
+ if isinstance (task , str ):
625
+ name_to_task [task ] = YoTask .load (task )
626
+ else :
627
+ name_to_task [task .name ] = task
628
+
629
+ # Tasks may have dependencies. Let's go through every task, and their
630
+ # dependencies, and load them all. At this point, we're not yet checking
631
+ # whether there are any circular dependencies: just loading them.
632
+ tasks_to_load = list (name_to_task .values ())
633
+ for task in tasks_to_load :
634
+ for name in task .dependencies :
635
+ if name not in name_to_task :
636
+ name_to_task [name ] = YoTask .load (name )
637
+ tasks_to_load .append (name_to_task [name ])
638
+
639
+ # Now we have loaded the complete set of tasks that should run. Some tasks
640
+ # may appoint themselves as "prerequisites" for another. We need to insert
641
+ # this dependency relationship so that the script is updated, and so that
642
+ # the circular dependency detection knows about it. We can also use this
643
+ # opportunity to detect conflicts.
644
+ for task in name_to_task .values ():
645
+ for name in task .prereq_for :
646
+ if name in name_to_task :
647
+ name_to_task [name ].insert_prereq (task .name )
648
+ for name in task .conflicts :
649
+ if name in name_to_task :
650
+ raise YoExc (f"Task { task .name } conflicts with { name } " )
651
+
652
+ # Now all tasks are loaded, and prerequisites have been marked. Use a
653
+ # topological sort to verify that no circular dependencies are present. Here
654
+ # we use a recursive traversal because honestly, if you specify enough tasks
655
+ # to trigger a recursion error, then I would like to receive that bug
656
+ # report!
617
657
name_to_visit : t .Dict [str , int ] = collections .defaultdict (int )
618
- # output ordering
619
658
ordered_tasks : t .List [YoTask ] = []
620
659
621
- def visit (task_or_name : t .Union [YoTask , str ]) -> None :
622
- if isinstance (task_or_name , str ):
623
- name : str = task_or_name
624
- task : t .Optional [YoTask ] = None
625
- else :
626
- name = task_or_name .name
627
- task = task_or_name
628
- if name_to_visit [name ] == 2 :
660
+ def visit (task : YoTask ) -> None :
661
+ if name_to_visit [task .name ] == 2 :
629
662
# already completed, skip
630
663
return
631
- if name_to_visit [name ] == 1 :
664
+ if name_to_visit [task . name ] == 1 :
632
665
# currently visiting, not a DAG
633
666
raise YoExc ("Tasks express a circular dependency" )
634
667
635
- name_to_visit [name ] = 1
636
- if not task :
637
- task = YoTask .load (name )
668
+ name_to_visit [task .name ] = 1
638
669
for dep_name in task .dependencies :
639
- visit (dep_name )
640
- name_to_visit [name ] = 2
670
+ visit (name_to_task [ dep_name ] )
671
+ name_to_visit [task . name ] = 2
641
672
ordered_tasks .append (task )
642
673
643
- for name in tasks :
644
- visit (name )
645
-
646
- # Now ordered_tasks contains the order in which we should launch them.
647
- # Verify that there are no conflicts
648
- all_task_names = {t .name for t in ordered_tasks }
649
- for task in ordered_tasks :
650
- for conflict in task .conflicts :
651
- if conflict in all_task_names :
652
- raise YoExc (f"Task { task } conflicts with { conflict } " )
674
+ for task in name_to_task .values ():
675
+ visit (task )
653
676
654
- # Do the thing!
677
+ # Now ordered_tasks contains the order in which we should launch them. This
678
+ # is just a nice-to-have: even if we launched them out of order, the
679
+ # DEPENDS_ON function would enforce the order of execution. Regardless,
680
+ # let's start the tasks.
655
681
for task in ordered_tasks :
656
682
_task_run (ctx , inst , task )
657
683
0 commit comments