def test_recipes_split_across_blocks(self) -> None: compiled = compile_markdown( dedent(""" A recipe in two parts. Part one: sauce = boil down(tomatoes, water) Part two: pour over(pasta, sauce) """).strip()) sr = SubRecipe( Step( SVS("boil down"), ( Ingredient(SVS("tomatoes")), Ingredient(SVS("water")), ), ), (SVS("sauce"), ), ) r1 = Recipe((sr, )) r2 = Recipe( (Step( SVS("pour over"), ( Ingredient(SVS("pasta")), Reference(sr), ), ), ), follows=r1, ) assert compiled.recipes == [[r1, r2]]
def test_scale(self) -> None: step = Step(SVS(["fry in ", 4, " blocks"]), (Ingredient(SVS("spam"), Quantity(2)), )) assert step.scale(3) == Step( SVS(["fry in ", 12, " blocks"]), (Ingredient(SVS("spam"), Quantity(6)), ), )
def test_compilation_of_steps(self) -> None: assert compile(["fry(slice(spam), eggs)"]) == [ Recipe((Step( SVS("fry"), ( Step(SVS("slice"), (Ingredient(SVS("spam")), )), Ingredient(SVS("eggs")), ), ), )), ]
def test_inlining_single_references_name_explicitly_required(self) -> None: assert compile(["meat := spam, sliced\nfry(meat, eggs)"]) == [ Recipe((Step( SVS("fry"), ( SubRecipe( Step(SVS("sliced"), (Ingredient(SVS("spam")), )), (SVS("meat"), ), ), Ingredient(SVS("eggs")), ), ), )) ]
def test_inlining_single_references(self, quantity_spec: str) -> None: assert compile( [f"meat = 10g spam, sliced\nfry({quantity_spec} meat, eggs)"]) == [ Recipe((Step( SVS("fry"), ( Step( SVS("sliced"), (Ingredient(SVS("spam"), Quantity(10, "g")), ), ), Ingredient(SVS("eggs")), ), ), )) ]
def test_separate_recipes(self) -> None: compiled = compile_markdown( dedent(""" Fried egg: ```recipe 1 egg ``` ```recipe fry(egg) ``` Boiled egg: ```new-recipe 2 egg ``` ```recipe boil(egg) ``` """).strip()) e1 = SubRecipe( Ingredient(SVS("egg"), Quantity(1)), (SVS("egg"), ), show_output_names=False, ) r1 = Recipe((e1, )) r2 = Recipe((Step(SVS("fry"), (Reference(e1), )), ), follows=r1) e2 = SubRecipe( Ingredient(SVS("egg"), Quantity(2)), (SVS("egg"), ), show_output_names=False, ) r3 = Recipe((e2, )) r4 = Recipe((Step(SVS("boil"), (Reference(e2), )), ), follows=r3) assert compiled.recipes == [[r1, r2], [r3, r4]] # Check anchor IDs are unique too html = compiled.render() assert 'id="recipe-egg"' in html assert 'href="#recipe-egg"' in html assert 'id="recipe2-egg"' in html assert 'href="#recipe2-egg"' in html
def test_reference_compilation(self) -> None: sub_recipe = SubRecipe( Step(SVS("open"), (Ingredient(SVS("spam")), )), (SVS("spam"), SVS("tin")), True, ) assert compile([ "spam, tin = open(spam)\nspam\n1/3*spam\n25% of the spam\nleft over spam\n2 'tin'\n50g spam" # noqa: E501 ]) == [ Recipe(( sub_recipe, # spam Reference(sub_recipe, 0, Proportion(1.0)), # 1/3*spam Reference(sub_recipe, 0, Proportion(Fraction(1, 3), preposition="*")), # 25% of the spam Reference( sub_recipe, 0, Proportion(0.25, percentage=True, preposition="% of the"), ), # remaining Reference(sub_recipe, 0, Proportion(None, remainder_wording="left over")), # 2 tin Reference(sub_recipe, 1, Quantity(2.0)), # 50g spam Reference(sub_recipe, 0, Quantity(50.0, "g")), )), ]
def test_nested_reference_to_sub_recipe_not_in_recipe(self) -> None: external_sr = SubRecipe(Ingredient(SVS("eggs")), (SVS("foo"), )) ref = Reference(external_sr) step = Step(SVS("bar"), (ref, )) with pytest.raises(ReferenceToInvalidSubRecipeError): Recipe((step, ))
def test_reference_to_nested_sub_recipe(self) -> None: nested_sr = SubRecipe(Ingredient(SVS("eggs")), (SVS("foo"), )) step = Step(SVS("scramble"), (nested_sr, )) ref = Reference(nested_sr) with pytest.raises(ReferenceToInvalidSubRecipeError): Recipe((step, ref))
def test_inlines_within_inlines(self) -> None: recipe = Recipe((Step( SVS("boil"), ( SubRecipe( Step( SVS("fry"), (Ingredient(SVS("spam"), Quantity(100, "g")), ), ), (SVS("fried spam"), ), ), Ingredient(SVS("water")), ), ), )) assert compile([ "100g spam\nfried spam := fry(spam)\nboil(fried spam, water)" ]) == [recipe]
def test_processed_ingredient_with_implied_output_name( self, syntax: str) -> None: assert compile([syntax]) == [ Recipe((SubRecipe( Step(SVS("fry"), (Ingredient(SVS("spam")), )), (SVS("spam"), ), False, ), )), ]
def test_step() -> None: input_0 = Ingredient(SVS("input 0")) input_1 = Ingredient(SVS("input 1")) input_2_ingredient = Ingredient(SVS("input 2")) input_2 = Step(SVS("chopped"), (input_2_ingredient, )) step = Step(SVS("combine"), (input_0, input_1, input_2)) assert recipe_tree_to_table(step) == set_border_around_table( Table.from_dict( cast( Mapping[Tuple[int, int], Cell[RecipeTreeNode]], { (0, 0): Cell(input_0, columns=2), (1, 0): Cell(input_1, columns=2), (2, 0): Cell(input_2_ingredient), (2, 1): Cell(input_2), (0, 2): Cell(step, rows=3), }, )), BorderType.sub_recipe, )
def test_single_output_sub_recipe_hidden() -> None: ingredient = Ingredient(SVS("spam")) step = Step(SVS("fry"), (ingredient, )) sub_recipe = SubRecipe(step, (SVS("out"), ), show_output_names=False) assert recipe_tree_to_table(sub_recipe) == set_border_around_table( Table.from_dict( cast( Mapping[Tuple[int, int], Cell[RecipeTreeNode]], { (0, 0): Cell(ingredient), (0, 1): Cell(step) }, )), BorderType.sub_recipe, )
def test_substitute(self) -> None: a = Ingredient(SVS("a")) b = Ingredient(SVS("b")) c = Ingredient(SVS("c")) d = Ingredient(SVS("d")) orig = Step(SVS("stir"), (a, b)) assert orig.substitute(a, c) == Step(SVS("stir"), (c, b)) assert orig.substitute(orig, c) == c assert orig.substitute(d, c) == orig
def test_dont_inline_partial_uses_of_a_subrecipe(self) -> None: sub_recipe = SubRecipe( Ingredient(SVS("spam"), Quantity(100, "g")), (SVS("spam"), ), False, ) assert compile(["100g spam\nfry(50g spam, eggs)"]) == [ Recipe(( sub_recipe, Step( SVS("fry"), ( Reference(sub_recipe, 0, Quantity(50, "g")), Ingredient(SVS("eggs")), ), ), )) ]
def test_dont_inline_multi_output_subrecipes(self) -> None: sub_recipe = SubRecipe( Ingredient(SVS("spam")), (SVS("meat"), SVS("tin")), True, ) assert compile(["meat, tin = spam\nfry(meat, eggs)"]) == [ Recipe(( sub_recipe, Step( SVS("fry"), ( Reference(sub_recipe, 0), Ingredient(SVS("eggs")), ), ), )) ]
def test_inlining_within_a_block(self) -> None: recipe0 = Recipe((SubRecipe(Ingredient(SVS("egg")), (SVS("egg"), ), show_output_names=False), )) recipe1 = Recipe( (Step(SVS("fry"), (Ingredient(SVS("spam"), Quantity(50, "g")), )), ), follows=recipe0, ) recipe2 = Recipe( (SubRecipe(Ingredient(SVS("potato")), (SVS("potato"), ), show_output_names=False), ), follows=recipe1, ) assert compile(["egg", "50g spam\nfry(spam)", "potato"]) == [ recipe0, recipe1, recipe2, ]
def test_dont_inline_definitions_from_earlier_blocks(self) -> None: sub_recipe = SubRecipe( Ingredient(SVS("spam"), Quantity(100, "g")), (SVS("spam"), ), False, ) recipe0 = Recipe((sub_recipe, )) recipe1 = Recipe( (Step( SVS("fry"), ( Reference(sub_recipe, 0, Quantity(50, "g")), Ingredient(SVS("eggs")), ), ), ), follows=recipe0, ) assert compile(["100g spam", "fry(50g spam, eggs)"]) == [recipe0, recipe1]
def test_render_table(id: Optional[str], exp_attrs: str) -> None: assert render_table( Table.from_dict({ (0, 0): Cell(Ingredient(SVS("spam"))), (1, 0): Cell(Ingredient(SVS("eggs"))), (0, 1): Cell( Step(SVS("fry"), (Ingredient(SVS("spam")), Ingredient(SVS("eggs")))), rows=2, ), }), id=id, ) == (f'<table class="rg-table"{exp_attrs}>\n' " <tr>\n" ' <td class="rg-ingredient">spam</td>\n' ' <td class="rg-step" rowspan="2">fry</td>\n' " </tr>\n" ' <tr><td class="rg-ingredient">eggs</td></tr>\n' "</table>")
def test_multiple_output_sub_recipe() -> None: ingredient_0 = Ingredient(SVS("spam")) ingredient_1 = Ingredient(SVS("eggs")) ingredient_2 = Ingredient(SVS("more spam")) step = Step(SVS("fry"), (ingredient_0, ingredient_1, ingredient_2)) sub_recipe = SubRecipe(step, (SVS("output 0"), SVS("output 1"))) assert recipe_tree_to_table(sub_recipe) == combine_tables( cast( List[Table[RecipeTreeNode]], [ set_border_around_table( Table.from_dict( cast( Mapping[Tuple[int, int], Cell[RecipeTreeNode]], { (0, 0): Cell(ingredient_0), (1, 0): Cell(ingredient_1), (2, 0): Cell(ingredient_2), (0, 1): Cell(step, rows=3), }, )), BorderType.sub_recipe, ), Table.from_dict({ (0, 0): Cell( sub_recipe, rows=3, border_top=BorderType.none, border_right=BorderType.none, border_bottom=BorderType.none, ) }), ], ), axis=1, )
def test_recipe_code_blocks_and_scaled_rendering(self, source: str) -> None: compiled = compile_markdown(dedent(source).strip()) assert compiled.title == "A recipe" assert compiled.servings == 2 assert compiled.recipes == [[ Recipe((Step( SVS("fry"), ( Ingredient(SVS("spam"), Quantity(100, "g")), Ingredient(SVS("eggs"), Quantity(2)), ), ), )), ]] assert (compiled.render(2) == dedent(""" <header><h1 class="rg-title-scalable">A recipe <span class="rg-serving-count">for <span class="rg-scaled-value">4</span></span></h1><p>Rescaled from <span class="rg-original-servings">2 servings</span>.</p></header> <div class="rg-recipe-block"> <table class="rg-table"> <tr> <td class="rg-ingredient rg-border-left-sub-recipe rg-border-top-sub-recipe"> <span class="rg-quantity-with-conversions rg-scaled-value" tabindex="0"> 200g<ul class="rg-quantity-conversions"> <li><sup>1</sup>⁄<sub>5</sub>kg</li> <li>0.441lb</li> <li>7.05oz</li> </ul> </span> spam </td> <td class="rg-step rg-border-right-sub-recipe rg-border-top-sub-recipe rg-border-bottom-sub-recipe" rowspan="2">fry</td> </tr> <tr><td class="rg-ingredient rg-border-left-sub-recipe rg-border-bottom-sub-recipe"><span class="rg-quantity-unitless rg-scaled-value">4</span> eggs</td></tr> </table> </div><p>Ta-da!</p> """ # noqa: E501 ).lstrip())
def test_step_with_multiple_inputs_has_no_inferred_name(self) -> None: assert compile(["fry(spam, eggs)"]) == [ Recipe( (Step(SVS("fry"), (Ingredient(SVS("spam")), Ingredient(SVS("eggs")))), )), ]
def test_rowspan(self) -> None: assert (render_cell( Cell( Step(SVS("fry"), (Ingredient(SVS("spam")), )), rows=3, ), ) == '<td class="rg-step" rowspan="3">fry</td>')
def test_step(self) -> None: assert (render_cell(Cell(Step( SVS("fry"), (Ingredient(SVS("spam")), )))) == '<td class="rg-step">fry</td>')
def test_render_step() -> None: assert render_step(Step(SVS("fry"), (Ingredient(SVS("spam")), ))) == "fry"
def _compile_step(self, ast_step: ast.Step) -> Step: return Step( description=compile_string(ast_step.name), inputs=tuple( self._compile_expr(ast_expr) for ast_expr in ast_step.inputs), )
def test_normalise_output_name() -> None: # Just a sanity check assert normalise_output_name(SVS(" Foo ")) == normalise_output_name( SVS("fOo")) assert normalise_output_name(SVS(" Foo ")) != normalise_output_name( SVS("bAr")) @pytest.mark.parametrize( "recipe_tree, exp", [ # Workable cases (Ingredient(SVS("spam")), SVS("spam")), (Step(SVS("fry"), (Ingredient(SVS("spam")), )), SVS("spam")), # Can't name step with several inputs (Step(SVS("fry"), (Ingredient(SVS("spam")), Ingredient(SVS("eggs")))), None), # Can't name references (Reference(SubRecipe(Ingredient(SVS("spam")), (SVS("spam"), )), 0), None), # Can't name sub recipes (they're already named!) (SubRecipe(Ingredient(SVS("spam")), (SVS("spam"), )), None), ], ) def test_infer_output_name(recipe_tree: RecipeTreeNode, exp: Optional[SVS]) -> None: assert infer_output_name(recipe_tree) == exp