diff --git a/.gitignore b/.gitignore index 4ab112d7d..c51bb9687 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .idea +*.iml .venv* .pvenv .nox diff --git a/sphinx_needs/directives/need.py b/sphinx_needs/directives/need.py index 202b0f07c..2015bdcaa 100644 --- a/sphinx_needs/directives/need.py +++ b/sphinx_needs/directives/need.py @@ -340,6 +340,13 @@ def analyse_need_locations(app: Sphinx, doctree: nodes.document) -> None: if need_node.get("hidden"): hidden_needs.append(need_node) + if need_node_excluded_by_only_directive(env, need_node): + # the need is excluded as contained in an "only" directive evaluated to False for current tags + if hasattr(env, "needs_all_needs"): + del env.needs_all_needs[need_id] + if need_node.parent is not None and hasattr(need_node.parent, "remove"): + need_node.parent.remove(need_node) + # now we have gathered all the information we need, # we can remove the hidden needs from the doctree for need_node in hidden_needs: @@ -356,6 +363,18 @@ def previous_sibling(node: nodes.Node) -> Optional[nodes.Node]: return node.parent[i - 1] if i > 0 else None # type: ignore +def need_node_excluded_by_only_directive(env: BuildEnvironment, node: nodes.Node) -> bool: + """Return True if the node is under an "only" directive that shall be excluded given to the current tags""" + parent = node.parent + while parent is not None: + if hasattr(parent, "tagname") and parent.tagname == "only" and hasattr(parent, "attributes"): + # note: we do not manage nested only directive + only_tags = parent.attributes.get("expr", "") + return not env.app.builder.tags.eval_condition(only_tags) + parent = parent.parent + return False + + @profile("NEEDS_POST_PROCESS") @measure_time("need_post_process") def post_process_needs_data(app: Sphinx) -> None: diff --git a/tests/doc_test/doc_directive_only/conf.py b/tests/doc_test/doc_directive_only/conf.py new file mode 100644 index 000000000..bd0af22b1 --- /dev/null +++ b/tests/doc_test/doc_directive_only/conf.py @@ -0,0 +1,4 @@ +extensions = ["sphinx_needs"] + +# also build the needs.json as some needs shall be hidden in it too +needs_build_json = True diff --git a/tests/doc_test/doc_directive_only/index.rst b/tests/doc_test/doc_directive_only/index.rst new file mode 100644 index 000000000..9a4831aff --- /dev/null +++ b/tests/doc_test/doc_directive_only/index.rst @@ -0,0 +1,53 @@ +Only directive Test +===================== + +.. req:: always_here_1 + :id: REQ_000 + + out of only is alwasy here + +.. only:: tag_a + + .. req:: only_tag_a + :id: REQ_001 + + I shall not appear if not running tag_a + + .. req:: only_tag_a_again + :id: REQ_001_1 + + need within need under only, shall neither appear if not running tag_a + + +.. only:: tag_b + + .. req:: only_tag_b + :id: REQ_002 + + I shall not appear if not running tag_b + + +.. only:: tag_a or tag_b + + .. req:: only_tag_a_or_b + :id: REQ_003 + + I shall not appear if not running either tag_a or tag_b + + +.. req:: always_here_2 + :id: REQ_004 + + I shall always appear + + +Needs table +-------------- + +.. needtable:: + + +Needs list +-------------- +.. needlist:: + diff --git a/tests/test_directive_only_exclusion.py b/tests/test_directive_only_exclusion.py new file mode 100644 index 000000000..8da7348b6 --- /dev/null +++ b/tests/test_directive_only_exclusion.py @@ -0,0 +1,100 @@ +import json +from pathlib import Path + +import pytest + + +def is_need_present(need_id, html, needs_list, is_present=True): + assert ( + is_need_in_html(need_id, html) is is_present + ), f"{need_id} should {'not ' if not is_present else ''}be present in html" + + assert ( + is_need_in_needtable(need_id, html) is is_present + ), f"{need_id} should {'not ' if not is_present else ''}be present in needtable" + + assert ( + is_need_in_needlist(need_id, html) is is_present + ), f"{need_id} should {'not ' if not is_present else ''}be present in needlist" + + assert ( + need_id in needs_list + ) is is_present, f"{need_id} should {'not ' if not is_present else ''}be present in needs.json" + + +def is_need_in_html(need_id, html): + return 'id="' + need_id + '">' in html + + +def is_need_in_needtable(need_id, html): + return ( + '

' + need_id + "

" + in html + ) + + +def is_need_in_needlist(need_id, html): + return '
' + need_id + ":" in html + + +def get_json_needs(needs_file: Path): + with open(needs_file) as file: + data = json.load(file) + return data["versions"][data["current_version"]]["needs"] + + +@pytest.mark.parametrize( + "test_app", + [ + {"buildername": "html", "srcdir": "doc_test/doc_directive_only", "tags": ["tag_a"]}, + ], + indirect=True, +) +def test_need_excluded_under_only_a(test_app): + app = test_app + app.build() + html = Path(app.outdir, "index.html").read_text() + needs_list = get_json_needs(Path(app.outdir, "needs.json")) + + is_need_present("REQ_000", html, needs_list) + is_need_present("REQ_001", html, needs_list) + is_need_present("REQ_001_1", html, needs_list) + is_need_present("REQ_002", html, needs_list, False) + is_need_present("REQ_003", html, needs_list) + is_need_present("REQ_004", html, needs_list) + + +@pytest.mark.parametrize( + "test_app", + [ + {"buildername": "html", "srcdir": "doc_test/doc_directive_only", "tags": ["tag_b"]}, + ], + indirect=True, +) +def test_need_excluded_under_only_b(test_app): + app = test_app + app.build() + html = Path(app.outdir, "index.html").read_text() + needs_list = get_json_needs(Path(app.outdir, "needs.json")) + + is_need_present("REQ_000", html, needs_list) + is_need_present("REQ_001", html, needs_list, False) + is_need_present("REQ_001_1", html, needs_list, False) + is_need_present("REQ_002", html, needs_list) + is_need_present("REQ_003", html, needs_list) + is_need_present("REQ_004", html, needs_list) + + +@pytest.mark.parametrize("test_app", [{"buildername": "html", "srcdir": "doc_test/doc_directive_only"}], indirect=True) +def test_need_excluded_under_only_no_tag(test_app): + app = test_app + app.build() + html = Path(app.outdir, "index.html").read_text() + needs_list = get_json_needs(Path(app.outdir, "needs.json")) + + is_need_present("REQ_000", html, needs_list) + is_need_present("REQ_001", html, needs_list, False) + is_need_present("REQ_001_1", html, needs_list, False) + is_need_present("REQ_002", html, needs_list, False) + is_need_present("REQ_003", html, needs_list, False) + is_need_present("REQ_004", html, needs_list)