diff --git a/docs/directives/list2need.rst b/docs/directives/list2need.rst index 924a3182a..6656e397a 100644 --- a/docs/directives/list2need.rst +++ b/docs/directives/list2need.rst @@ -168,6 +168,27 @@ tags The tags ``A`` and ``B`` are attached to all ``NEED-A``, ``NEED-B``, ``NEED-C`` and ``NEED-D``. +list-options +~~~~~~~~~~~~ + +``list-options`` allows to set common options for all needs in the list. + +.. code-block:: rst + + .. list2need:: + :types: req, spec + :list-options: + :hide: + :status: open + :validated_by: TEST-005 + + * (NEED-A)Login user + * (NEED-B)Provide login screen + * (NEED-C)Create password hash ((validated_by="TEST-006")) + * (NEED-D)Recalculate hash and compare + +All the ``hide``, ``status=open`` and ``validated_by=TEST-005-A`` and are attached to all ``NEED-A``, ``NEED-B``, ``NEED-C`` and ``NEED-D``. +Same options are aggregate: the ``NEED-C`` has a ``validated_by`` option set to ``TEST-005,TEST-006``. List examples ------------- diff --git a/sphinx_needs/directives/list2need.py b/sphinx_needs/directives/list2need.py index ec50b6b12..b44cb75a3 100644 --- a/sphinx_needs/directives/list2need.py +++ b/sphinx_needs/directives/list2need.py @@ -58,6 +58,7 @@ def presentation(argument: str) -> Any: "presentation": directives.unchanged, "links-down": directives.unchanged, "tags": directives.unchanged, + "list-options": directives.unchanged, } def run(self) -> Sequence[nodes.Node]: @@ -110,6 +111,7 @@ def run(self) -> Sequence[nodes.Node]: # Retrieve tags defined at list level tags = self.options.get("tags", "") + list_options = self.options.get("list-options", "") list_needs = [] # Storing the data in a sorted list @@ -170,6 +172,7 @@ def run(self) -> Sequence[nodes.Node]: "content": content.lstrip(), "level": level, "options": {}, + "list_options": {}, } list_needs.append(need) else: @@ -205,6 +208,27 @@ def run(self) -> Sequence[nodes.Node]: else: list_need["options"]["tags"] = tags + if list_options: + pattern = r":(\w+):\s*([^\n:]*)" + matches = re.findall(pattern, list_options) + for key, value in matches: + if "options" not in list_need: + list_need["options"] = {} + current_key = list_need["options"].get(key, "") + if current_key: + list_need["options"][key] = current_key + "," + value.strip() + else: + list_need["options"][key] = value.strip() + + # if "options" not in list_need: + # list_need["options"] = {} + # current_list_options = list_need["options"] + # + # if current_list_options: + # list_need["options"] = current_list_options + "," + list_options + # else: + # list_need["options"] = list_options + template = Template(NEED_TEMPLATE, autoescape=True) data = list_need diff --git a/tests/doc_test/doc_list2need_list_options/conf.py b/tests/doc_test/doc_list2need_list_options/conf.py new file mode 100644 index 000000000..db49ce342 --- /dev/null +++ b/tests/doc_test/doc_list2need_list_options/conf.py @@ -0,0 +1,44 @@ +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"] diff --git a/tests/doc_test/doc_list2need_list_options/index.rst b/tests/doc_test/doc_list2need_list_options/index.rst new file mode 100644 index 000000000..36f1dab22 --- /dev/null +++ b/tests/doc_test/doc_list2need_list_options/index.rst @@ -0,0 +1,20 @@ +TEST DOCUMENT LIST2NEED +======================= + + +.. list2need:: + :types: spec, spec + :tags: list_of_needs + :list-options: + :hide: + :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_list_options.py b/tests/test_list2need_list_options.py new file mode 100644 index 000000000..afc282748 --- /dev/null +++ b/tests/test_list2need_list_options.py @@ -0,0 +1,40 @@ +import json +from pathlib import Path + +import pytest + + +@pytest.mark.parametrize( + "test_app", + [ + { + "buildername": "needs", + "srcdir": "doc_test/doc_list2need_list_options", + "confoverrides": {"needs_reproducible_json": True}, + } + ], + indirect=True, +) +def test_doc_list2need_list_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" + )