diff --git a/scone/head/grammar/scoml.tx b/scone/head/grammar/scoml.tx index eb9e6b8..8090227 100644 --- a/scone/head/grammar/scoml.tx +++ b/scone/head/grammar/scoml.tx @@ -27,7 +27,8 @@ SubBlock[ws=' \t']: Directive: UserDirective | SousDirective | ForDirective | ImportDirective | - RecipeEdgeDirective | ResourceEdgeDirective | ListenEdgeDirective + RecipeEdgeDirective | ResourceEdgeDirective | ListenEdgeDirective | + IfSetDirective ; UserDirective[ws=' \t']: @@ -53,6 +54,10 @@ ForDirective[ws=' \t']: ) ; +IfSetDirective[ws=' \t']: + '@ifSet' variable=DottedIdString /\n/+ +; + ResourceEdgeDirectiveKind: '@needs' | '@wants' | '@provides' ; diff --git a/scone/head/menu_reader.py b/scone/head/menu_reader.py index e8c496e..7aa3c43 100644 --- a/scone/head/menu_reader.py +++ b/scone/head/menu_reader.py @@ -45,8 +45,13 @@ scoml_classes = scoml_grammar.namespaces["scoml"] logger = logging.getLogger(__name__) +class ControlDirective: + def iter_over(self, vars: Variables) -> Iterable[Variables]: + raise NotImplementedError("Abstract.") + + @attr.s(auto_attribs=True) -class ForDirective: +class ForDirective(ControlDirective): """ For loop_variable in collection """ @@ -57,6 +62,41 @@ class ForDirective: # List of literals or str for a variable (by name) collection: Union[str, List[Any]] + def iter_over(self, vars: Variables): + to_iter = self.collection + if isinstance(to_iter, str): + to_iter = vars.get_dotted(to_iter) + + if not isinstance(to_iter, list): + raise ValueError(f"to_iter = {to_iter!r} not a list") + + for item in to_iter: + new_vars = Variables(vars) + new_vars.set_dotted(self.loop_variable, item) + yield new_vars + + +@attr.s(auto_attribs=True) +class IfDirective(ControlDirective): + def condition_true(self, vars: Variables) -> bool: + return False + + def iter_over(self, vars: Variables) -> Iterable[Variables]: + if self.condition_true(vars): + yield vars + else: + yield from () + + +@attr.s(auto_attribs=True) +class IfSetDirective(IfDirective): + # Name of the variable to check for existence. + check_variable: str + + def condition_true(self, vars: Variables) -> bool: + print(f"isset? {self.check_variable} {vars.has_dotted(self.check_variable)}") + return vars.has_dotted(self.check_variable) + @attr.s(auto_attribs=True) class RecipeEdgeDirective: @@ -94,7 +134,7 @@ class MenuBlock: user_directive: Optional[str] = None sous_directive: Optional[str] = None - for_directives: List[ForDirective] = attr.ib(factory=list) + control_directives: List[ForDirective] = attr.ib(factory=list) import_directives: List[str] = attr.ib(factory=list) recipe_edges: List[RecipeEdgeDirective] = attr.ib(factory=list) resource_edges: List[ResourceEdgeDirective] = attr.ib(factory=list) @@ -114,7 +154,7 @@ class MenuRecipe: user_directive: Optional[str] = None sous_directive: Optional[str] = None - for_directives: List[ForDirective] = attr.ib(factory=list) + control_directives: List[ForDirective] = attr.ib(factory=list) recipe_edges: List[RecipeEdgeDirective] = attr.ib(factory=list) resource_edges: List[ResourceEdgeDirective] = attr.ib(factory=list) listen_edges: List[ListenEdgeDirective] = attr.ib(factory=list) @@ -183,6 +223,18 @@ def convert_textx_recipe(txrecipe_or_subblock, parent: Optional[MenuBlock]): or convert_textx_resource(directive.resource), ) ) + elif isinstance(directive, scoml_classes["ForDirective"]): + for_list = directive.collection or convert_textx_value(directive.list) + assert isinstance(for_list, list) or isinstance(for_list, str) + recipe.control_directives.append( + ForDirective(directive.loop_variable, for_list) + ) + elif isinstance(directive, scoml_classes["IfSetDirective"]): + var = directive.variable + assert isinstance(var, str) + recipe.control_directives.append( + IfSetDirective(var) + ) else: raise ValueError(f"Unknown directive {directive}") @@ -224,8 +276,14 @@ def convert_textx_block(txblock, parent: Optional[MenuBlock]) -> MenuBlock: elif isinstance(directive, scoml_classes["ForDirective"]): for_list = directive.collection or convert_textx_value(directive.list) assert isinstance(for_list, list) or isinstance(for_list, str) - block.for_directives.append( - ForDirective(directive.loop_variable, for_list,) + block.control_directives.append( + ForDirective(directive.loop_variable, for_list) + ) + elif isinstance(directive, scoml_classes["IfSetDirective"]): + var = directive.variable + assert isinstance(var, str) + block.control_directives.append( + IfSetDirective(var) ) elif isinstance(directive, scoml_classes["ImportDirective"]): block.import_directives.append(directive.importee) @@ -342,7 +400,7 @@ class MenuLoader: a: Union[MenuBlock, MenuRecipe] = referrer strip = 0 while a != first_common_ancestor: - strip += len(a.for_directives) + strip += len(a.control_directives) parent = a.parent assert parent is not None a = parent @@ -350,7 +408,7 @@ class MenuLoader: a = menu_recipe extra = 0 while a != first_common_ancestor: - extra += len(a.for_directives) + extra += len(a.control_directives) parent = a.parent assert parent is not None a = parent @@ -394,7 +452,7 @@ class MenuLoader: if recipe_class is None: raise ValueError(f"No recipe class found for {recipe.kind!r}") - fors = fors + tuple(recipe.for_directives) + fors = fors + tuple(recipe.control_directives) if recipe.user_directive: applicable_user = recipe.user_directive @@ -411,7 +469,7 @@ class MenuLoader: assert applicable_user is not None sous_vars = self._head.variables[sous] - for context_vars, for_indices in self._for_apply(fors, sous_vars, tuple()): + for context_vars, for_indices in self._control_apply(fors, sous_vars, tuple()): context = RecipeContext( sous=sous, user=applicable_user, @@ -439,7 +497,7 @@ class MenuLoader: sous_mask: Optional[Set[str]], applicable_user: Optional[str], ): - fors = fors + tuple(block.for_directives) + fors = fors + tuple(block.control_directives) if block.user_directive: applicable_user = block.user_directive @@ -483,7 +541,7 @@ class MenuLoader: # TODO(feature): add edges # add fors - fors = fors + tuple(recipe.for_directives) + fors = fors + tuple(recipe.control_directives) if recipe.sous_directive: applicable_souss = self._head.get_souss_for_hostspec(recipe.sous_directive) @@ -493,7 +551,7 @@ class MenuLoader: for sous in applicable_souss: sous_vars = self._head.variables[sous] - for _vars, for_indices in self._for_apply(fors, sous_vars, tuple()): + for _vars, for_indices in self._control_apply(fors, sous_vars, tuple()): instance = self._recipes[recipe][(sous, for_indices)] # noqa for recipe_edge in recipe.recipe_edges: @@ -573,7 +631,7 @@ class MenuLoader: # TODO(feature): add edges - fors = fors + tuple(block.for_directives) + fors = fors + tuple(block.control_directives) if block.sous_directive: applicable_souss = self._head.get_souss_for_hostspec(block.sous_directive) @@ -604,27 +662,18 @@ class MenuLoader: unit, tuple(), self._head.get_souss_for_hostspec("all"), sous_subset ) - def _for_apply( - self, fors: Tuple[ForDirective, ...], vars: "Variables", accum: Tuple[int, ...] + def _control_apply( + self, controls: Tuple[ControlDirective, ...], vars: "Variables", accum: Tuple[int, ...] ) -> Iterable[Tuple["Variables", Tuple[int, ...]]]: - if not fors: + if not controls: yield vars, accum return - head = fors[0] - tail = fors[1:] + head = controls[0] + tail = controls[1:] - to_iter = head.collection - if isinstance(to_iter, str): - to_iter = vars.get_dotted(to_iter) - - if not isinstance(to_iter, list): - raise ValueError(f"to_iter = {to_iter!r} not a list") - - for idx, item in enumerate(to_iter): - new_vars = Variables(vars) - new_vars.set_dotted(head.loop_variable, item) - yield from self._for_apply(tail, new_vars, accum + (idx,)) + for idx, new_vars in enumerate(head.iter_over(vars)): + yield from self._control_apply(tail, new_vars, accum + (idx,)) def load_menus_in_dir(self) -> RecipeDag: dag = RecipeDag()