From f9580db229df47d3707b756552d9dc8f5f8e795a Mon Sep 17 00:00:00 2001 From: vodka Date: Wed, 30 Oct 2024 15:52:16 +0100 Subject: [PATCH 1/8] add hide and meta-data options --- sphinx_needs/directives/list2need.py | 43 ++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/sphinx_needs/directives/list2need.py b/sphinx_needs/directives/list2need.py index ec50b6b12..ed1b9d5d0 100644 --- a/sphinx_needs/directives/list2need.py +++ b/sphinx_needs/directives/list2need.py @@ -13,6 +13,12 @@ from sphinx_needs.config import NeedsSphinxConfig + +from sphinx_needs.logging import get_logger, log_warning + +logger = get_logger(__name__) + + NEED_TEMPLATE = """.. {{type}}:: {{title}} {% if need_id is not none %}:id: {{need_id}}{%endif%} {% if set_links_down %}:{{links_down_type}}: {{ links_down|join(', ') }}{%endif%} @@ -58,6 +64,8 @@ def presentation(argument: str) -> Any: "presentation": directives.unchanged, "links-down": directives.unchanged, "tags": directives.unchanged, + "hide": directives.unchanged, + "meta-data": directives.unchanged } def run(self) -> Sequence[nodes.Node]: @@ -110,6 +118,17 @@ def run(self) -> Sequence[nodes.Node]: # Retrieve tags defined at list level tags = self.options.get("tags", "") + hide = self.options.get("hide", "") + metadata = self.options.get("meta-data", "") + + if metadata: + log_warning( + logger, + metadata, + "needsequence", + location=None, + + ) list_needs = [] # Storing the data in a sorted list @@ -205,6 +224,30 @@ def run(self) -> Sequence[nodes.Node]: else: list_need["options"]["tags"] = tags + if hide: + if "options" not in list_need: + list_need["options"] = {} + hide_option = list_need["options"].get("hide", "") + list_need["options"]["hide"] = hide_option + + + if metadata: + if "options" not in list_need: + list_need["options"] = {} + metadata_items = re.findall(r'([^=,]+)=["\']([^"\']+)["\']', metadata) + + for key, value in metadata_items: + current_options = list_need["options"].get(key.strip(), "") + + + + if current_options: + list_need["options"][key.strip()] = current_options + "," + value + else: + list_need["options"][key.strip()] = value + + + template = Template(NEED_TEMPLATE, autoescape=True) data = list_need From 8add581865f31f6360427a41affee90144c18185 Mon Sep 17 00:00:00 2001 From: vodka Date: Wed, 30 Oct 2024 16:01:14 +0100 Subject: [PATCH 2/8] Add documentation about bith new options --- docs/directives/list2need.rst | 56 +++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/docs/directives/list2need.rst b/docs/directives/list2need.rst index 924a3182a..ccf210f10 100644 --- a/docs/directives/list2need.rst +++ b/docs/directives/list2need.rst @@ -169,6 +169,62 @@ tags The tags ``A`` and ``B`` are attached to all ``NEED-A``, ``NEED-B``, ``NEED-C`` and ``NEED-D``. + +hide +~~~~ + +``hide`` sets the hide-option globally to all items in the list. + +.. code-block:: rst + + .. list2need:: + :types: req + :tags: A + :hide: True + + * (NEED-A) Login user + * (NEED-B) Provide login screen + * (NEED-C) Create password hash + * (NEED-D) Recalculate hash and compare + +All ``NEED-A``, ``NEED-B``, ``NEED-C`` and ``NEED-D`` requirements will be marked as hidden. This allows to easily create a list of requirements and presenting them as a table in the final output. + +.. code-block:: rst + + .. list2need:: + :types: req + :tags: A + :hide: True + + * (NEED-A) Login user + * (NEED-B) Provide login screen + * (NEED-C) Create password hash + * (NEED-D) Recalculate hash and compare + + .. needtable:: + :types: req + :tags: A + :style: table + :columns: id, title, content, links + + +meta-data +~~~~~~~~~ + +Meta-data can be set directly in the related line via, for example: ``((status="open"))``. This meta-data option allows to define meta-data that will be affected to all needs in the list, including extra custom options. + +.. code-block:: rst + + .. list2need:: + :types: req + :tags: A + :meta-data: validation="Test, Review of Design", status="open" + + * (NEED-A) Login user + * (NEED-B) Provide login screen + * (NEED-C) Create password hash + * (NEED-D) Recalculate hash and compare + List examples ------------- From 9f0939ac9801d889f263a022896e76d956258407 Mon Sep 17 00:00:00 2001 From: vodka Date: Wed, 30 Oct 2024 16:02:30 +0100 Subject: [PATCH 3/8] Remove forgotten log message --- sphinx_needs/directives/list2need.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/sphinx_needs/directives/list2need.py b/sphinx_needs/directives/list2need.py index ed1b9d5d0..5861f10bd 100644 --- a/sphinx_needs/directives/list2need.py +++ b/sphinx_needs/directives/list2need.py @@ -13,12 +13,6 @@ from sphinx_needs.config import NeedsSphinxConfig - -from sphinx_needs.logging import get_logger, log_warning - -logger = get_logger(__name__) - - NEED_TEMPLATE = """.. {{type}}:: {{title}} {% if need_id is not none %}:id: {{need_id}}{%endif%} {% if set_links_down %}:{{links_down_type}}: {{ links_down|join(', ') }}{%endif%} @@ -121,15 +115,6 @@ def run(self) -> Sequence[nodes.Node]: hide = self.options.get("hide", "") metadata = self.options.get("meta-data", "") - if metadata: - log_warning( - logger, - metadata, - "needsequence", - location=None, - - ) - list_needs = [] # Storing the data in a sorted list for content_line in content_raw.split("\n"): From 0079b814d2980e10dde1949e31845c880134f6ed Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 30 Oct 2024 15:08:51 +0000 Subject: [PATCH 4/8] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- sphinx_needs/directives/list2need.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/sphinx_needs/directives/list2need.py b/sphinx_needs/directives/list2need.py index 5861f10bd..216a8b904 100644 --- a/sphinx_needs/directives/list2need.py +++ b/sphinx_needs/directives/list2need.py @@ -59,7 +59,7 @@ def presentation(argument: str) -> Any: "links-down": directives.unchanged, "tags": directives.unchanged, "hide": directives.unchanged, - "meta-data": directives.unchanged + "meta-data": directives.unchanged, } def run(self) -> Sequence[nodes.Node]: @@ -215,23 +215,20 @@ def run(self) -> Sequence[nodes.Node]: hide_option = list_need["options"].get("hide", "") list_need["options"]["hide"] = hide_option - if metadata: if "options" not in list_need: list_need["options"] = {} metadata_items = re.findall(r'([^=,]+)=["\']([^"\']+)["\']', metadata) - + for key, value in metadata_items: current_options = list_need["options"].get(key.strip(), "") - - if current_options: - list_need["options"][key.strip()] = current_options + "," + value + list_need["options"][key.strip()] = ( + current_options + "," + value + ) else: list_need["options"][key.strip()] = value - - template = Template(NEED_TEMPLATE, autoescape=True) From f4ec1a92545146f147ec768c27d8fa4047087150 Mon Sep 17 00:00:00 2001 From: "OFFICE\\cs" Date: Wed, 11 Dec 2024 11:23:23 +0100 Subject: [PATCH 5/8] Rename meta-data to list_global_options and remove unneeded code --- sphinx_needs/directives/list2need.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/sphinx_needs/directives/list2need.py b/sphinx_needs/directives/list2need.py index 216a8b904..89e4288af 100644 --- a/sphinx_needs/directives/list2need.py +++ b/sphinx_needs/directives/list2need.py @@ -59,7 +59,7 @@ def presentation(argument: str) -> Any: "links-down": directives.unchanged, "tags": directives.unchanged, "hide": directives.unchanged, - "meta-data": directives.unchanged, + "list_global_options": directives.unchanged, } def run(self) -> Sequence[nodes.Node]: @@ -113,7 +113,7 @@ def run(self) -> Sequence[nodes.Node]: # Retrieve tags defined at list level tags = self.options.get("tags", "") hide = self.options.get("hide", "") - metadata = self.options.get("meta-data", "") + global_options = self.options.get("list_global_options", "") list_needs = [] # Storing the data in a sorted list @@ -212,15 +212,14 @@ def run(self) -> Sequence[nodes.Node]: if hide: if "options" not in list_need: list_need["options"] = {} - hide_option = list_need["options"].get("hide", "") - list_need["options"]["hide"] = hide_option + list_need["options"]["hide"] = hide - if metadata: + if global_options: if "options" not in list_need: list_need["options"] = {} - metadata_items = re.findall(r'([^=,]+)=["\']([^"\']+)["\']', metadata) + global_options_items = re.findall(r'([^=,]+)=["\']([^"\']+)["\']', global_options) - for key, value in metadata_items: + for key, value in global_options_items: current_options = list_need["options"].get(key.strip(), "") if current_options: From a82427d2d6e55962e0294483133d7d6827ef3ff5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 11 Dec 2024 10:24:05 +0000 Subject: [PATCH 6/8] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- sphinx_needs/directives/list2need.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sphinx_needs/directives/list2need.py b/sphinx_needs/directives/list2need.py index 89e4288af..1d4d489ec 100644 --- a/sphinx_needs/directives/list2need.py +++ b/sphinx_needs/directives/list2need.py @@ -217,7 +217,9 @@ def run(self) -> Sequence[nodes.Node]: if global_options: if "options" not in list_need: list_need["options"] = {} - global_options_items = re.findall(r'([^=,]+)=["\']([^"\']+)["\']', global_options) + global_options_items = re.findall( + r'([^=,]+)=["\']([^"\']+)["\']', global_options + ) for key, value in global_options_items: current_options = list_need["options"].get(key.strip(), "") From 44dfc2bd61aaed4d9c54e20eca4cffdb0b0b3d77 Mon Sep 17 00:00:00 2001 From: "OFFICE\\cs" Date: Thu, 12 Dec 2024 09:20:17 +0100 Subject: [PATCH 7/8] Fix some bugs and add tests --- pyproject.toml | 2 +- sphinx_needs/directives/list2need.py | 26 +++++++---- tests/doc_test/doc_list2need_hide/conf.py | 43 +++++++++++++++++ tests/doc_test/doc_list2need_hide/index.rst | 28 +++++++++++ .../doc_list2need_list_global_options/conf.py | 46 +++++++++++++++++++ .../index.rst | 16 +++++++ tests/test_list2need.py | 4 ++ tests/test_list2need_hide.py | 32 +++++++++++++ tests/test_list2need_list_global_options.py | 43 +++++++++++++++++ 9 files changed, 231 insertions(+), 9 deletions(-) create mode 100644 tests/doc_test/doc_list2need_hide/conf.py create mode 100644 tests/doc_test/doc_list2need_hide/index.rst create mode 100644 tests/doc_test/doc_list2need_list_global_options/conf.py create mode 100644 tests/doc_test/doc_list2need_list_global_options/index.rst create mode 100644 tests/test_list2need_hide.py create mode 100644 tests/test_list2need_list_global_options.py diff --git a/pyproject.toml b/pyproject.toml index 60b0493c4..95df17e30 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ name = "sphinx-needs" # !! Don't miss updates in sphinx_needs.__version__, changelog.rst, and .github/workflows/docker !!! -version = "4.1.0" +version = "4.2.0" description = "Sphinx needs extension for managing needs/requirements and specifications" authors = ["team useblocks "] diff --git a/sphinx_needs/directives/list2need.py b/sphinx_needs/directives/list2need.py index 1d4d489ec..0ebef1b41 100644 --- a/sphinx_needs/directives/list2need.py +++ b/sphinx_needs/directives/list2need.py @@ -17,8 +17,9 @@ {% if need_id is not none %}:id: {{need_id}}{%endif%} {% if set_links_down %}:{{links_down_type}}: {{ links_down|join(', ') }}{%endif%} {%- for name, value in options.items() %}:{{name}}: {{value}} - {% endfor %} - + {% endfor %}{% if need_hide %}:hide:{%endif%} + + {{content}} """ @@ -63,6 +64,13 @@ def presentation(argument: str) -> Any: } def run(self) -> Sequence[nodes.Node]: + """_Implementation details_ + + The directive is used to create a list of needs (list_needs). Each list entry is used to create a single need using + a jinja2 template (Template). The template is defined in the NEED_TEMPLATE variable. The template is rendered for each list entry + + """ + env = self.env needs_config = NeedsSphinxConfig(env.config) @@ -112,7 +120,12 @@ def run(self) -> Sequence[nodes.Node]: # Retrieve tags defined at list level tags = self.options.get("tags", "") - hide = self.options.get("hide", "") + + if "hide" in self.options: + hide = True + else: + hide = False + global_options = self.options.get("list_global_options", "") list_needs = [] @@ -209,11 +222,8 @@ def run(self) -> Sequence[nodes.Node]: else: list_need["options"]["tags"] = tags - if hide: - if "options" not in list_need: - list_need["options"] = {} - list_need["options"]["hide"] = hide - + list_need["need_hide"] = hide + if global_options: if "options" not in list_need: list_need["options"] = {} diff --git a/tests/doc_test/doc_list2need_hide/conf.py b/tests/doc_test/doc_list2need_hide/conf.py new file mode 100644 index 000000000..df8b630a7 --- /dev/null +++ b/tests/doc_test/doc_list2need_hide/conf.py @@ -0,0 +1,43 @@ +extensions = ["sphinx_needs"] +project = "test for list2need hide" +author = 'Christophe SEYLER' + +needs_table_style = "TABLE" + +needs_id_regex = "^[A-Za-z0-9_]" + +needs_types = [ + { + "directive": "story", + "title": "User Story", + "prefix": "US_", + "color": "#BFD8D2", + "style": "node", + }, + { + "directive": "spec", + "title": "Specification", + "prefix": "SP_", + "color": "#FEDCD2", + "style": "node", + }, + { + "directive": "impl", + "title": "Implementation", + "prefix": "IM_", + "color": "#DF744A", + "style": "node", + }, + { + "directive": "test", + "title": "Test Case", + "prefix": "TC_", + "color": "#DCB239", + "style": "node", + }, +] + +needs_extra_links = [ + {"option": "checks", "incoming": "is checked by", "outgoing": "checks"}, + {"option": "triggers", "incoming": "is triggered by", "outgoing": "triggers"}, +] diff --git a/tests/doc_test/doc_list2need_hide/index.rst b/tests/doc_test/doc_list2need_hide/index.rst new file mode 100644 index 000000000..f2cff429b --- /dev/null +++ b/tests/doc_test/doc_list2need_hide/index.rst @@ -0,0 +1,28 @@ +TEST DOCUMENT LIST2NEED +======================= + + +.. list2need:: + :types: spec, spec + :tags: tag1 + :hide: + + * (NEED-A) Need example on level 1. fsdfsdf. ((status="closed",tags="tag2")) + * (NEED-B) Link example ((status="closed",tags="tag2")) + * (NEED-B-1) Need example on level 2 + * (NEED-C) New line example. ((status="closed",tags="tag2")) + With some content in the next line. + + + +List +==== + +.. needtable:: Example table + :tags: tag1 + :style: table + :columns: id + :show_filters: + + +.. _test: diff --git a/tests/doc_test/doc_list2need_list_global_options/conf.py b/tests/doc_test/doc_list2need_list_global_options/conf.py new file mode 100644 index 000000000..3c1c799aa --- /dev/null +++ b/tests/doc_test/doc_list2need_list_global_options/conf.py @@ -0,0 +1,46 @@ +extensions = ["sphinx_needs", "sphinxcontrib.plantuml"] +project = "test for list2need list_global_options" +author = 'Christophe SEYLER' + +needs_table_style = "TABLE" + +needs_id_regex = "^[A-Za-z0-9_]" + +needs_types = [ + { + "directive": "story", + "title": "User Story", + "prefix": "US_", + "color": "#BFD8D2", + "style": "node", + }, + { + "directive": "spec", + "title": "Specification", + "prefix": "SP_", + "color": "#FEDCD2", + "style": "node", + }, + { + "directive": "impl", + "title": "Implementation", + "prefix": "IM_", + "color": "#DF744A", + "style": "node", + }, + { + "directive": "test", + "title": "Test Case", + "prefix": "TC_", + "color": "#DCB239", + "style": "node", + }, +] + +needs_extra_links = [ + {"option": "checks", "incoming": "is checked by", "outgoing": "checks"}, + {"option": "triggers", "incoming": "is triggered by", "outgoing": "triggers"}, +] +needs_extra_options = [ + "aggregateoption" +] \ No newline at end of file diff --git a/tests/doc_test/doc_list2need_list_global_options/index.rst b/tests/doc_test/doc_list2need_list_global_options/index.rst new file mode 100644 index 000000000..1f57c9d40 --- /dev/null +++ b/tests/doc_test/doc_list2need_list_global_options/index.rst @@ -0,0 +1,16 @@ +TEST DOCUMENT LIST2NEED +======================= + + +.. list2need:: + :types: spec, spec + :list_global_options: status="open", aggregateoption="SomeValue" + + * (NEED-A) Need example on level 1 + * (NEED-B) Need example on level 1 + * (NEED-C) Link example + * (NEED-C-1) Need example on level 2 + * (NEED-D) New line example. ((aggregateoption="OtherValue")) + With some content in the next line. + +.. _test: diff --git a/tests/test_list2need.py b/tests/test_list2need.py index d4c272227..5b7452839 100644 --- a/tests/test_list2need.py +++ b/tests/test_list2need.py @@ -2,9 +2,12 @@ import pytest + from sphinx_needs.api import get_needs_view + + @pytest.mark.parametrize( "test_app", [{"buildername": "html", "srcdir": "doc_test/doc_list2need"}], @@ -68,3 +71,4 @@ def test_doc_list2need_html(test_app, snapshot): 'href="#NEED-B" title="NEED-C">NEED-B' in links_down_html ) + diff --git a/tests/test_list2need_hide.py b/tests/test_list2need_hide.py new file mode 100644 index 000000000..683944dd6 --- /dev/null +++ b/tests/test_list2need_hide.py @@ -0,0 +1,32 @@ +from pathlib import Path + +import pytest +import json + +from sphinx_needs.api import get_needs_view + + + + +@pytest.mark.parametrize( + "test_app", + [{"buildername": "html", "srcdir": "doc_test/doc_list2need_hide"}], + indirect=True, +) +def test_doc_list2need_hide(test_app, snapshot): + app = test_app + app.build() + + index_html = Path(app.outdir, "index.html").read_text() + assert '

NEED-A

' in index_html + assert '

NEED-B

' in index_html + assert '

NEED-C

' in index_html + + + assert 'class="need_container docutils container"' not in index_html + + + + + + \ No newline at end of file diff --git a/tests/test_list2need_list_global_options.py b/tests/test_list2need_list_global_options.py new file mode 100644 index 000000000..4f9617688 --- /dev/null +++ b/tests/test_list2need_list_global_options.py @@ -0,0 +1,43 @@ +from pathlib import Path + +import pytest +import json + +from sphinx_needs.api import get_needs_view + + + + +@pytest.mark.parametrize( + "test_app", + [ + { + "buildername": "needs", + "srcdir": "doc_test/doc_list2need_list_global_options", + "confoverrides": {"needs_reproducible_json": True}, + } + ], + indirect=True, +) +def test_doc_list2need_list_global_options(test_app, snapshot): + app = test_app + app.build() + + needs_list = json.loads(Path(app.outdir, "needs.json").read_text()) + + needs = needs_list["versions"][""]["needs"] + + # Check that all entries have a status item equal to "open" + for need_id, need in needs.items(): + assert need.get("status") == "open", f"Need {need_id} does not have status 'open'" + assert "SomeValue" in need.get("aggregateoption", ""), f"Need {need_id} does not have 'SomeValue' in aggregateoption" + + # Check that NEED-D has "OtherValue" in its aggregateoption + need_d = needs.get("NEED-D") + assert need_d is not None, "NEED-D is missing" + assert "OtherValue" in need_d.get("aggregateoption", ""), "NEED-D does not have 'OtherValue' in aggregateoption" + + + + + \ No newline at end of file From a25760ec215ddd6f5f06506f75e14bf9d6a94c30 Mon Sep 17 00:00:00 2001 From: "OFFICE\\cs" Date: Thu, 12 Dec 2024 11:12:46 +0100 Subject: [PATCH 8/8] add a test description --- tests/test_list2need_hide.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_list2need_hide.py b/tests/test_list2need_hide.py index 683944dd6..e16c0d760 100644 --- a/tests/test_list2need_hide.py +++ b/tests/test_list2need_hide.py @@ -14,9 +14,17 @@ indirect=True, ) def test_doc_list2need_hide(test_app, snapshot): + """ + The test validates the list2need directive with the hide option. + The needs must be valid, but the rendered output must not contain the need content. + + To validate that the needs are valid, the needs are rendered using a needtable directive. + """ app = test_app app.build() + + index_html = Path(app.outdir, "index.html").read_text() assert '

NEED-A

' in index_html assert '

NEED-B

' in index_html