def test_all_edge_kinds(self) -> None: assert set_border_around_table( Table([ [Cell(100), Cell(101), Cell(102)], [Cell(110), Cell(111), Cell(112)], [Cell(120), Cell(121), Cell(122)], ]), BorderType.none, ) == Table([ [ Cell(100, border_left=BorderType.none, border_top=BorderType.none), Cell(101, border_top=BorderType.none), Cell(102, border_top=BorderType.none, border_right=BorderType.none), ], [ Cell(110, border_left=BorderType.none), Cell(111), Cell(112, border_right=BorderType.none), ], [ Cell(120, border_left=BorderType.none, border_bottom=BorderType.none), Cell(121, border_bottom=BorderType.none), Cell(122, border_bottom=BorderType.none, border_right=BorderType.none), ], ])
def test_validation_extended_cell_in_wrong_place(self) -> None: # Top left corner with pytest.raises(CellExpectedError): Table([[ExtendedCell(Cell(123), 0, 0)]]) # Elsewhere description: List[List[Union[Cell[int], ExtendedCell[int]]]] = [[ Cell(123), ExtendedCell(Cell(123), 0, 1) ]] with pytest.raises(CellExpectedError): Table(description)
def test_validation_extended_cell_bad_coordinate(self) -> None: cell = Cell(123, 1, 2) description: List[List[Union[Cell[int], ExtendedCell[int]]]] description = [[cell, ExtendedCell(cell, 1, 1)]] with pytest.raises(ExtendedCellCoordinateError): Table(description) description = [[cell, ExtendedCell(cell, 0, 0)]] with pytest.raises(ExtendedCellCoordinateError): Table(description)
def test_expand_single_cell(self) -> None: t1 = Table.from_dict({(0, 0): Cell(123)}) assert right_pad_table(t1, 2) == Table.from_dict({ (0, 0): Cell(123, columns=2) }) t2 = Table.from_dict({(0, 0): Cell(123, columns=2)}) assert right_pad_table(t2, 4) == Table.from_dict({ (0, 0): Cell(123, columns=4) })
def test_already_wide_enough(self) -> None: # Two cells t1 = Table([[Cell(123), Cell(123)]]) assert right_pad_table(t1, 2) == t1 # A wide cell t2 = Table.from_dict({(0, 0): Cell(123, columns=2)}) assert right_pad_table(t2, 2) == t2 # Wider than needed t3 = Table.from_dict({(0, 0): Cell(123, columns=3)}) assert right_pad_table(t3, 2) == t3
def test_single_cell(self) -> None: assert set_border_around_table( Table.from_dict({(0, 0): Cell(123)}), BorderType.none, ) == Table.from_dict({ (0, 0): Cell( 123, border_left=BorderType.none, border_right=BorderType.none, border_top=BorderType.none, border_bottom=BorderType.none, ), })
def test_expand_multiple_cells_including_rows_with_only_extended_cells( self, ) -> None: t1 = Table.from_dict({ (0, 0): Cell(123, columns=2, rows=2), (2, 0): Cell(123), (2, 1): Cell(123), }) assert right_pad_table(t1, 4) == Table.from_dict({ (0, 0): Cell(123, columns=4, rows=2), (2, 0): Cell(123), (2, 1): Cell(123, columns=3), })
def test_validation_extended_cell_bad_reference(self) -> None: description: List[List[Union[Cell[int], ExtendedCell[int]]]] = [[ Cell(123, columns=2), ExtendedCell(Cell(123), 0, 1) ]] with pytest.raises(ExtendedCellReferenceError): Table(description)
def test_mismatched_shapes(self) -> None: with pytest.raises(MissingCellError): combine_tables( [ Table.from_dict({(0, 0): Cell(123, columns=2)}), Table.from_dict({(0, 0): Cell(123)}), ], axis=0, ) with pytest.raises(MissingCellError): combine_tables( [ Table.from_dict({(0, 0): Cell(123, rows=2)}), Table.from_dict({(0, 0): Cell(123)}), ], axis=1, )
def test_to_dict(self) -> None: c10 = Cell("1,0") c11 = Cell("1,1") c0x = Cell("0,x", 1, 2) d = { (0, 0): c0x, (1, 0): c10, (1, 1): c11, } assert Table.from_dict(d).to_dict() == d
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_indexing(self) -> None: # Minimal case c00 = Cell("0,0") t = Table([[c00]]) assert t[0, 0] == c00 assert t.rows == 1 assert t.columns == 1 # Plain cells only c01 = Cell("0,1") c10 = Cell("1,0") c11 = Cell("1,1") t = Table([[c00, c01], [c10, c11]]) assert t[0, 0] == c00 assert t[0, 1] == c01 assert t[1, 0] == c10 assert t[1, 1] == c11 assert t.rows == 2 assert t.columns == 2 # With extended cells c0x = Cell("1,x", 1, 2) t = Table([[c0x, ExtendedCell(c0x, 0, 1)], [c10, c11]]) assert t[0, 0] == c0x assert t[0, 1] == ExtendedCell(c0x, 0, 1) assert t[1, 0] == c10 assert t[1, 1] == c11 assert t.rows == 2 assert t.columns == 2 # With asymmetry c0x = Cell("1,x", 1, 2) t = Table([[c0x, ExtendedCell(c0x, 0, 1)]]) assert t[0, 0] == c0x assert t[0, 1] == ExtendedCell(c0x, 0, 1) assert t.rows == 1 assert t.columns == 2
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_from_dict(self) -> None: # Minimal case c00 = Cell("0,0") assert Table.from_dict({(0, 0): c00}) == Table([[c00]]) # Plain cells only c01 = Cell("0,1") c10 = Cell("1,0") c11 = Cell("1,1") assert Table.from_dict({ (0, 0): c00, (0, 1): c01, (1, 0): c10, (1, 1): c11 }) == Table([[c00, c01], [c10, c11]]) # With Simple extended cell cxx = Cell("0,x", 2, 3) description: List[List[Union[Cell[str], ExtendedCell[str]]]] = [ [cxx, ExtendedCell(cxx, 0, 1), ExtendedCell(cxx, 0, 2)], [ ExtendedCell(cxx, 1, 0), ExtendedCell(cxx, 1, 1), ExtendedCell(cxx, 1, 2), ], ] assert Table.from_dict({(0, 0): cxx}) == Table(description) # With normal and extended cells c0x = Cell("0,x", 1, 2) description = [[c0x, ExtendedCell(c0x, 0, 1)], [c10, c11]] assert Table.from_dict({ (0, 0): c0x, (1, 0): c10, (1, 1): c11 }) == Table(description)
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_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_tables(self) -> None: t1 = Table.from_dict({(0, 0): Cell(123, columns=2)}) t2 = Table.from_dict({(0, 0): Cell(123), (0, 1): Cell(123)}) assert combine_tables([t1, t2], axis=0) == Table.from_dict({ (0, 0): Cell(123, columns=2), (1, 0): Cell(123), (1, 1): Cell(123) }) t3 = Table.from_dict({(0, 0): Cell(123, rows=2)}) t4 = Table.from_dict({(0, 0): Cell(123), (1, 0): Cell(123)}) assert combine_tables([t3, t4], axis=1) == Table.from_dict({ (0, 0): Cell(123, rows=2), (0, 1): Cell(123), (1, 1): Cell(123) })
def test_ingredient() -> None: ingredient = Ingredient(SVS("spam")) assert recipe_tree_to_table(ingredient) == set_border_around_table( Table.from_dict({(0, 0): Cell(ingredient)}), BorderType.sub_recipe)
def recipe_tree_to_table( recipe_tree: RecipeTreeNode, _root: bool = True ) -> Table[RecipeTreeNode]: """ Convert a recipe tree into tabular form. To display the resulting :py:class:`~Table`, cell contents should be rendered as indicated below. Firstly, the obvious cases: * :py:class:`~Ingredient` is shown as the ingredient quantity and name etc. * :py:class:`~Reference` is shown as its label (and quantity/proportion etc.) * :py:class:`~Step` is shown as its description. The cells immediately to the left in the generated table will contain the inputs to this step. Finally, cells containing a :py:class:`~SubRecipe` may appear in the table for one of two purposes: * For single-output sub recipes, the cell containing the :py:class:`SubRecipe` will be located above the cells containing the body of the sub recipe. This cell should be rendered in the style of a heading and containing the output name as the text. * For multiple-output sub recipes, thec cell containing the :py:class:`SubRecipe` will be immediately to the right of the cells defining the sub-recipe and should be rendered as a list all of the output names for the sub recipe, for example as a bulleted list. .. note:: This cell will have all but its left border style set to be :py:attr:`BorderType.none` to give the appearance of the output list being located out to the right of the rest of the table. Unless otherwise stated, all cells will have all borders set to :py:attr:`BorderType.normal` with the exception of those borders of cells near the edges of a sub recipe block. These will have a style of :py:attr:`BorderType.sub_recipe`. .. note:: The ``_root`` argument is for internal use and must be left unspecified when called by external users. Internally, it indicates if the node passed to this function is the root node of its recipe tree. """ if isinstance(recipe_tree, (Ingredient, Reference)): table: Table[RecipeTreeNode] = Table([[Cell(recipe_tree)]]) if _root: return set_border_around_table(table, BorderType.sub_recipe) else: return table elif isinstance(recipe_tree, Step): input_tables = [ recipe_tree_to_table(input_tree, False) for input_tree in recipe_tree.inputs ] # Pad all input tables to same width and stack them up input_columns = max(table.columns for table in input_tables) input_tables = [right_pad_table(table, input_columns) for table in input_tables] combined_input_tables = combine_tables(input_tables, axis=0) # Add step to RHS of inputs table = combine_tables( [ combined_input_tables, Table.from_dict( {(0, 0): Cell(recipe_tree, rows=combined_input_tables.rows)} ), ], axis=1, ) if _root: return set_border_around_table(table, BorderType.sub_recipe) else: return table elif isinstance(recipe_tree, SubRecipe): sub_tree_table = recipe_tree_to_table(recipe_tree.sub_tree, False) if len(recipe_tree.output_names) == 1: if recipe_tree.show_output_names: return set_border_around_table( combine_tables( [ Table.from_dict( { (0, 0): Cell( cast(RecipeTreeNode, recipe_tree), columns=sub_tree_table.columns, ), } ), sub_tree_table, ], axis=0, ), BorderType.sub_recipe, ) else: return set_border_around_table(sub_tree_table, BorderType.sub_recipe) else: return combine_tables( [ set_border_around_table(sub_tree_table, BorderType.sub_recipe), Table.from_dict( { (0, 0): Cell( cast(RecipeTreeNode, recipe_tree), rows=sub_tree_table.rows, border_top=BorderType.none, border_right=BorderType.none, border_bottom=BorderType.none, ), } ), ], axis=1, ) else: raise NotImplementedError(type(recipe_tree))
def test_validation_ragged_rows(self) -> None: with pytest.raises(MissingCellError): Table([[Cell(123)], [Cell(123), Cell(123)]]) with pytest.raises(MissingCellError): Table([[Cell(123), Cell(123)], [Cell(123)]])
def test_validation_extended_cell_is_missing(self, rows: int, columns: int) -> None: with pytest.raises(MissingCellError): Table([[Cell(123, rows, columns)]])
def test_validation_extended_cell_is_cell(self, rows: int, columns: int) -> None: with pytest.raises(ExtendedCellExpectedError): Table([[Cell(123, rows, columns), Cell(123)], [Cell(123), Cell(123)]])
def test_from_dict_validation_empty(self) -> None: with pytest.raises(EmptyTableError): Table.from_dict({})
def test_validation_empty_table(self) -> None: with pytest.raises(EmptyTableError): Table([]) with pytest.raises(EmptyTableError): Table([[]])
def test_from_dict_validation_missing_cells(self, row: int, column: int) -> None: with pytest.raises(MissingCellError): Table.from_dict({(0, 0): Cell(123), (row, column): Cell(123)})
def test_single_table(self) -> None: orig = Table.from_dict({(0, 0): Cell(123, columns=2)}) assert combine_tables([orig], axis=0) == orig assert combine_tables([orig], axis=1) == orig
def test_reference() -> None: sub_recipe = SubRecipe(Ingredient(SVS("spam")), (SVS("out"), )) reference = Reference(sub_recipe, 0) assert recipe_tree_to_table(reference) == set_border_around_table( Table.from_dict({(0, 0): Cell(reference)}), BorderType.sub_recipe)