From 579b1aa85af404b44c58f8adf34da0f33b423f59 Mon Sep 17 00:00:00 2001 From: Benjamin Bossan Date: Tue, 31 Jan 2023 17:42:35 +0100 Subject: [PATCH 1/2] ENH Add possibility to turn section invisible Sections of model cards have an additional flag called visible. It is by default True but if set to False, the corresponding section, and its subsections, are not rendered. In contrast to deleting those section, turning them invisible will not remove them from the underlying data dict. This is useful because it allows us to restore those sections to exactly their previous place. In contrast, if we delete and add them again, they may end up in a different position, because the order of the underlying dict can change. At the moment, the feature is not used anywhere in skops. I plan, however, to use it for a model card creation space, where it would be quite useful to have. --- skops/card/_model_card.py | 9 ++- skops/card/tests/test_card.py | 101 ++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 1 deletion(-) diff --git a/skops/card/_model_card.py b/skops/card/_model_card.py index 65e27299..a60ae5f4 100644 --- a/skops/card/_model_card.py +++ b/skops/card/_model_card.py @@ -243,16 +243,20 @@ class Section: empty string) or a ``Formattable``, which is simply an object with a ``format`` method that returns a string. - Finally, the section can contain subsections, which again are dicts of + The section can contain subsections, which again are dicts of string keys and section values (the dict can be empty). Therefore, the model card representation forms a tree structure, making use of the fact that dict order is preserved. + The section may also contain a ``visible`` flag, which determined if the + section will be shown when the card is rendered. + """ title: str content: Formattable | str subsections: dict[str, Section] = field(default_factory=dict) + visible: bool = True def select(self, key: str) -> Section: """Return a subsection or subsubsection of this section @@ -1182,6 +1186,9 @@ def _generate_content( """ for val in data.values(): + if not val.visible: + continue + title = f"{depth * '#'} {val.title}" yield title diff --git a/skops/card/tests/test_card.py b/skops/card/tests/test_card.py index 5c3731f1..9ad94277 100644 --- a/skops/card/tests/test_card.py +++ b/skops/card/tests/test_card.py @@ -1492,3 +1492,104 @@ def test_custom_template_all_sections_present(self, template, card): # no other top level sections as those defined in the template expected = ["My description", "Model", "Foo"] assert list(card._data.keys()) == expected + + +class TestRenderedCardVisibility: + """Check that visibility flag works + + Sections that are not visible should not be rendered, neither when calling + model_card.render, nor when calling model_card.save. + + """ + + @pytest.fixture + def template(self): + return { + "Model": "Here goes model related stuff", + "Model/Metrics": "123", + "Model/Bar": "Baz", + "Authors": "Jane Doe", + } + + @pytest.fixture + def card(self, template): + model = fit_model() + card = Card(model, template=template) + return card + + def test_all_visible_by_default(self, card): + rendered = card.render() + expected = ( + "# Model\n\n" + "Here goes model related stuff\n\n" + "## Metrics\n\n" + "123\n\n" + "## Bar\n\n" + "Baz\n\n" + "# Authors\n\n" + "Jane Doe" + ) + assert rendered.strip() == expected + + def test_section_invisible(self, card): + card.select("Model/Metrics").visible = False + rendered = card.render() + expected = ( + "# Model\n\n" + "Here goes model related stuff\n\n" + "## Bar\n\n" + "Baz\n\n" + "# Authors\n\n" + "Jane Doe" + ) + assert rendered.strip() == expected + + def test_restoring_visibility_works(self, card): + card.select("Model/Metrics").visible = False + card.select("Model/Metrics").visible = True + expected = ( + "# Model\n\n" + "Here goes model related stuff\n\n" + "## Metrics\n\n" + "123\n\n" + "## Bar\n\n" + "Baz\n\n" + "# Authors\n\n" + "Jane Doe" + ) + rendered = card.render() + assert rendered.strip() == expected + + def test_invisible_parent_section_hides_subsections(self, card): + # By making the parent section "Model" invisible, all of the subsections + # are also turned invisible + card.select("Model").visible = False + # fmt: off + expected = ( + "# Authors\n\n" + "Jane Doe" + ) + # fmt: on + rendered = card.render() + assert rendered.strip() == expected + + def test_visibility_with_card_save(self, card): + # Since .save and .render share the same functionality, it's not + # necessary to repeat all the tests above with .save. Just do one test + # to ensure that the same functionality is indeed being used. + file = tempfile.mkstemp(suffix=".md", prefix="skops-model-card")[1] + card.select("Model/Metrics").visible = False + card.save(file) + + with open(file, "r") as f: + loaded = f.read() + + expected = ( + "# Model\n\n" + "Here goes model related stuff\n\n" + "## Bar\n\n" + "Baz\n\n" + "# Authors\n\n" + "Jane Doe" + ) + assert loaded.strip() == expected From fcf435cdaaf9bccadba85cb9a5ef5793833ab363 Mon Sep 17 00:00:00 2001 From: Benjamin Bossan Date: Tue, 31 Jan 2023 17:52:37 +0100 Subject: [PATCH 2/2] Fix typo in docstring --- skops/card/_model_card.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skops/card/_model_card.py b/skops/card/_model_card.py index a60ae5f4..d5e0e55c 100644 --- a/skops/card/_model_card.py +++ b/skops/card/_model_card.py @@ -248,7 +248,7 @@ class Section: card representation forms a tree structure, making use of the fact that dict order is preserved. - The section may also contain a ``visible`` flag, which determined if the + The section may also contain a ``visible`` flag, which determines if the section will be shown when the card is rendered. """