From 73b961e29583bc0ac8892ef9c4e2bfb75f7f46bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Kreuzberger?= Date: Wed, 24 Jan 2024 15:44:59 +0100 Subject: [PATCH] #947 filter_warning option to replace default "No needs passed the filters" text (#1093) Add option `:filter_warning:` to directives (e.g. needtable) to show no text or given text instead of the default text. --- docs/filter.rst | 19 ++ pyproject.toml | 5 + sphinx_needs/data.py | 2 + sphinx_needs/directives/needextract.py | 2 +- sphinx_needs/directives/needfilter.py | 7 +- sphinx_needs/directives/needflow.py | 7 +- sphinx_needs/directives/needgantt.py | 13 +- sphinx_needs/directives/needlist.py | 80 +++---- sphinx_needs/directives/needpie.py | 8 +- sphinx_needs/directives/needsequence.py | 9 +- sphinx_needs/directives/needtable.py | 20 +- sphinx_needs/directives/utils.py | 7 +- sphinx_needs/filter_common.py | 3 + tests/doc_test/filter_doc/conf.py | 23 +- tests/doc_test/filter_doc/filter.css | 5 + tests/doc_test/filter_doc/filter_no_needs.rst | 214 ++++++++++++++++++ tests/doc_test/filter_doc/index.rst | 1 + tests/test_filter.py | 29 +++ 18 files changed, 375 insertions(+), 79 deletions(-) create mode 100644 tests/doc_test/filter_doc/filter.css create mode 100644 tests/doc_test/filter_doc/filter_no_needs.rst diff --git a/docs/filter.rst b/docs/filter.rst index 6962cab7b..f13527e80 100644 --- a/docs/filter.rst +++ b/docs/filter.rst @@ -453,3 +453,22 @@ Example: results.append(cnt_x) results.append(cnt_y) + +Filter matches nothing +---------------------- + +Depending on the directive used a filter that matches no needs may add text to inform that no needs are found. + +The default text "No needs passed the filter". + +If this is not intended, add the option + +.. _option_filter_warning: + +filter_warning +~~~~~~~~~~~~~~ + +Add specific text with this option or add no text to display nothing. The default text will not be shown. + +The specified output could be styled with the css class ``needs_filter_warning`` + diff --git a/pyproject.toml b/pyproject.toml index 825bc07da..bfe10e434 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -151,6 +151,11 @@ module = [ ] disable_error_code = ["attr-defined", "no-any-return"] +[[tool.mypy.overrides]] +module = [ + "sphinx_needs.directives.needextract", +] +disable_error_code = "no-untyped-call" [build-system] requires = ["setuptools", "poetry_core>=1.0.8"] # setuptools for deps like plantuml diff --git a/sphinx_needs/data.py b/sphinx_needs/data.py index a69802d1f..465e626a4 100644 --- a/sphinx_needs/data.py +++ b/sphinx_needs/data.py @@ -266,6 +266,7 @@ class NeedsFilteredBaseType(NeedsBaseDataType): filter_code: list[str] filter_func: None | str export_id: str + filter_warning: str """If set, the filter is exported with this ID in the needs.json file.""" @@ -346,6 +347,7 @@ class NeedsPieType(NeedsBaseDataType): text_color: None | str shadow: bool filter_func: None | str + filter_warning: str class NeedsSequenceType(NeedsFilteredDiagramBaseType): diff --git a/sphinx_needs/directives/needextract.py b/sphinx_needs/directives/needextract.py index 246363705..b3ed41b67 100644 --- a/sphinx_needs/directives/needextract.py +++ b/sphinx_needs/directives/needextract.py @@ -120,7 +120,7 @@ def process_needextract( content.append(need_extract) if len(content) == 0: - content.append(no_needs_found_paragraph()) + content.append(no_needs_found_paragraph(current_needextract.get("filter_warning"))) if current_needextract["show_filters"]: content.append(used_filter_paragraph(current_needextract)) diff --git a/sphinx_needs/directives/needfilter.py b/sphinx_needs/directives/needfilter.py index f2e0a5eb2..3ef350e9c 100644 --- a/sphinx_needs/directives/needfilter.py +++ b/sphinx_needs/directives/needfilter.py @@ -11,6 +11,7 @@ from sphinx_needs.config import NeedsSphinxConfig from sphinx_needs.data import SphinxNeedsData from sphinx_needs.diagrams_common import create_legend +from sphinx_needs.directives.utils import no_needs_found_paragraph from sphinx_needs.filter_common import FilterBase, process_filters from sphinx_needs.utils import add_doc, remove_node_from_tree, row_col_maker @@ -228,11 +229,7 @@ def process_needfilters( content.append(puml_node) if len(content) == 0: - nothing_found = "No needs passed the filters" - para = nodes.line() - nothing_found_node = nodes.Text(nothing_found) - para += nothing_found_node - content.append(para) + content.append(no_needs_found_paragraph(current_needfilter.get("filter_warning"))) if current_needfilter["show_filters"]: para = nodes.paragraph() filter_text = "Used filter:" diff --git a/sphinx_needs/directives/needflow.py b/sphinx_needs/directives/needflow.py index 5bf3314f9..c67381514 100644 --- a/sphinx_needs/directives/needflow.py +++ b/sphinx_needs/directives/needflow.py @@ -19,6 +19,7 @@ ) from sphinx_needs.debug import measure_time from sphinx_needs.diagrams_common import calculate_link, create_legend +from sphinx_needs.directives.utils import no_needs_found_paragraph from sphinx_needs.filter_common import FilterBase, filter_single_need, process_filters from sphinx_needs.logging import get_logger from sphinx_needs.utils import ( @@ -457,11 +458,7 @@ def process_needflow(app: Sphinx, doctree: nodes.document, fromdocname: str, fou content.append(puml_node) else: # no needs found - nothing_found = "No needs passed the filters" - para = nodes.paragraph() - nothing_found_node = nodes.Text(nothing_found) - para += nothing_found_node - content.append(para) + content.append(no_needs_found_paragraph(current_needflow.get("filter_warning"))) if current_needflow["show_filters"]: para = nodes.paragraph() diff --git a/sphinx_needs/directives/needgantt.py b/sphinx_needs/directives/needgantt.py index a2dfea191..1ad22c3ce 100644 --- a/sphinx_needs/directives/needgantt.py +++ b/sphinx_needs/directives/needgantt.py @@ -20,7 +20,10 @@ get_filter_para, no_plantuml, ) -from sphinx_needs.directives.utils import SphinxNeedsLinkTypeException +from sphinx_needs.directives.utils import ( + SphinxNeedsLinkTypeException, + no_needs_found_paragraph, +) from sphinx_needs.filter_common import FilterBase, filter_single_need, process_filters from sphinx_needs.logging import get_logger from sphinx_needs.utils import MONTH_NAMES, add_doc, remove_node_from_tree @@ -312,12 +315,8 @@ def process_needgantt(app: Sphinx, doctree: nodes.document, fromdocname: str, fo content.append(puml_node) - if len(content) == 0: - nothing_found = "No needs passed the filters" - para = nodes.paragraph() - nothing_found_node = nodes.Text(nothing_found) - para += nothing_found_node - content.append(para) + if len(found_needs) == 0: + content = [no_needs_found_paragraph(current_needgantt.get("filter_warning"))] if current_needgantt["show_filters"]: content.append(get_filter_para(current_needgantt)) diff --git a/sphinx_needs/directives/needlist.py b/sphinx_needs/directives/needlist.py index 6d0fb7afd..25a19b433 100644 --- a/sphinx_needs/directives/needlist.py +++ b/sphinx_needs/directives/needlist.py @@ -83,48 +83,48 @@ def process_needlist(app: Sphinx, doctree: nodes.document, fromdocname: str, fou all_needs = list(SphinxNeedsData(env).get_or_create_needs().values()) found_needs = process_filters(app, all_needs, current_needfilter) - line_block = nodes.line_block() - - # Add lineno to node - line_block.line = current_needfilter["lineno"] - for need_info in found_needs: - para = nodes.line() - description = "{}: {}".format(need_info["id"], need_info["title"]) - - if current_needfilter["show_status"] and need_info["status"]: - description += " (%s)" % need_info["status"] - - if current_needfilter["show_tags"] and need_info["tags"]: - description += " [%s]" % "; ".join(need_info["tags"]) - - title = nodes.Text(description) - - # Create a reference - if need_info["hide"]: - para += title - elif need_info["is_external"]: - assert need_info["external_url"] is not None, "External need without URL" - ref = nodes.reference("", "") - - ref["refuri"] = check_and_calc_base_url_rel_path(need_info["external_url"], fromdocname) - - ref["classes"].append(need_info["external_css"]) - ref.append(title) - para += ref - else: - target_id = need_info["target_id"] - ref = nodes.reference("", "") - ref["refdocname"] = need_info["docname"] - ref["refuri"] = builder.get_relative_uri(fromdocname, need_info["docname"]) - ref["refuri"] += "#" + target_id - ref.append(title) - para += ref - line_block.append(para) - content.append(line_block) + if 0 < len(found_needs): + line_block = nodes.line_block() + + # Add lineno to node + line_block.line = current_needfilter["lineno"] + for need_info in found_needs: + para = nodes.line() + description = "{}: {}".format(need_info["id"], need_info["title"]) + + if current_needfilter["show_status"] and need_info["status"]: + description += " (%s)" % need_info["status"] + + if current_needfilter["show_tags"] and need_info["tags"]: + description += " [%s]" % "; ".join(need_info["tags"]) + + title = nodes.Text(description) + + # Create a reference + if need_info["hide"]: + para += title + elif need_info["is_external"]: + assert need_info["external_url"] is not None, "External need without URL" + ref = nodes.reference("", "") + + ref["refuri"] = check_and_calc_base_url_rel_path(need_info["external_url"], fromdocname) + + ref["classes"].append(need_info["external_css"]) + ref.append(title) + para += ref + else: + target_id = need_info["target_id"] + ref = nodes.reference("", "") + ref["refdocname"] = need_info["docname"] + ref["refuri"] = builder.get_relative_uri(fromdocname, need_info["docname"]) + ref["refuri"] += "#" + target_id + ref.append(title) + para += ref + line_block.append(para) + content.append(line_block) if len(content) == 0: - content.append(no_needs_found_paragraph()) - + content.append(no_needs_found_paragraph(current_needfilter.get("filter_warning"))) if current_needfilter["show_filters"]: content.append(used_filter_paragraph(current_needfilter)) diff --git a/sphinx_needs/directives/needpie.py b/sphinx_needs/directives/needpie.py index ab018ed8a..72a0affea 100644 --- a/sphinx_needs/directives/needpie.py +++ b/sphinx_needs/directives/needpie.py @@ -8,6 +8,7 @@ from sphinx_needs.config import NeedsSphinxConfig from sphinx_needs.data import SphinxNeedsData from sphinx_needs.debug import measure_time +from sphinx_needs.directives.utils import no_needs_found_paragraph from sphinx_needs.filter_common import FilterBase, filter_needs, prepare_need_list from sphinx_needs.logging import get_logger from sphinx_needs.utils import ( @@ -47,6 +48,7 @@ class NeedpieDirective(FilterBase): "text_color": directives.unchanged_required, "shadow": directives.flag, "filter-func": FilterBase.base_option_spec["filter-func"], + "filter_warning": FilterBase.base_option_spec["filter_warning"], } # Update the options_spec only with value filter-func defined in the FilterBase class @@ -94,6 +96,7 @@ def run(self) -> Sequence[nodes.Node]: "shadow": shadow, "text_color": text_color, "filter_func": self.collect_filter_attributes()["filter_func"], + "filter_warning": self.collect_filter_attributes()["filter_warning"], } add_doc(env, env.docname) @@ -273,7 +276,10 @@ def process_needpie(app: Sphinx, doctree: nodes.document, fromdocname: str, foun # Add lineno to node image_node.line = current_needpie["lineno"] - node.replace_self(image_node) + if len(sizes) == 0 or all(s == 0 for s in sizes): + node.replace_self(no_needs_found_paragraph(current_needpie.get("filter_warning"))) + else: + node.replace_self(image_node) # Cleanup matplotlib # Reset the style configuration: diff --git a/sphinx_needs/directives/needsequence.py b/sphinx_needs/directives/needsequence.py index 4e166be1c..4a9737175 100644 --- a/sphinx_needs/directives/needsequence.py +++ b/sphinx_needs/directives/needsequence.py @@ -19,6 +19,7 @@ get_filter_para, no_plantuml, ) +from sphinx_needs.directives.utils import no_needs_found_paragraph from sphinx_needs.filter_common import FilterBase from sphinx_needs.logging import get_logger from sphinx_needs.utils import add_doc, remove_node_from_tree @@ -209,12 +210,8 @@ def process_needsequence( content.append(puml_node) - if len(content) == 0: - nothing_found = "No needs passed the filters" - para = nodes.paragraph() - nothing_found_node = nodes.Text(nothing_found) - para += nothing_found_node - content.append(para) + if len(c_string) == 0 and p_string.count("participant") == 1: # no connections and just one (start) participant + content = [(no_needs_found_paragraph(current_needsequence.get("filter_warning")))] if current_needsequence["show_filters"]: content.append(get_filter_para(current_needsequence)) diff --git a/sphinx_needs/directives/needtable.py b/sphinx_needs/directives/needtable.py index ecab59642..c647f95a5 100644 --- a/sphinx_needs/directives/needtable.py +++ b/sphinx_needs/directives/needtable.py @@ -318,8 +318,15 @@ def sort(need: NeedsInfoType) -> Any: tbody += row if len(filtered_needs) == 0: - table_node.append(no_needs_found_paragraph()) - + content = no_needs_found_paragraph(current_needtable.get("filter_warning")) + else: + # Put the table in a div-wrapper, so that we can control overflow / scroll layout + if style == "TABLE": + table_wrapper = nodes.container(classes=["needstable_wrapper"]) + table_wrapper.insert(0, table_node) + content = table_wrapper + else: + content = table_node # add filter information to output if current_needtable["show_filters"]: table_node.append(used_filter_paragraph(current_needtable)) @@ -329,11 +336,4 @@ def sort(need: NeedsInfoType) -> Any: title = nodes.title(title_text, "", nodes.Text(title_text)) table_node.insert(0, title) - # Put the table in a div-wrapper, so that we can control overflow / scroll layout - if style == "TABLE": - table_wrapper = nodes.container(classes=["needstable_wrapper"]) - table_wrapper.insert(0, table_node) - node.replace_self(table_wrapper) - - else: - node.replace_self(table_node) + node.replace_self(content) diff --git a/sphinx_needs/directives/utils.py b/sphinx_needs/directives/utils.py index 4c33bc022..3efc786fb 100644 --- a/sphinx_needs/directives/utils.py +++ b/sphinx_needs/directives/utils.py @@ -1,5 +1,5 @@ import re -from typing import Any, Dict, List, Tuple +from typing import Any, Dict, List, Optional, Tuple from docutils import nodes from sphinx.environment import BuildEnvironment @@ -9,9 +9,10 @@ from sphinx_needs.defaults import TITLE_REGEX -def no_needs_found_paragraph() -> nodes.paragraph: - nothing_found = "No needs passed the filters" +def no_needs_found_paragraph(message: Optional[str]) -> nodes.paragraph: + nothing_found = "No needs passed the filters" if message is None else message para = nodes.paragraph() + para["classes"].append("needs_filter_warning") nothing_found_node = nodes.Text(nothing_found) para += nothing_found_node return para diff --git a/sphinx_needs/filter_common.py b/sphinx_needs/filter_common.py index e93a5ff40..20e1fe74b 100644 --- a/sphinx_needs/filter_common.py +++ b/sphinx_needs/filter_common.py @@ -34,6 +34,7 @@ class FilterAttributesType(TypedDict): filter_code: list[str] filter_func: str export_id: str + filter_warning: str """If set, the filter is exported with this ID in the needs.json file.""" @@ -48,6 +49,7 @@ class FilterBase(SphinxDirective): "filter-func": directives.unchanged_required, "sort_by": directives.unchanged, "export_id": directives.unchanged, + "filter_warning": directives.unchanged, } def collect_filter_attributes(self) -> FilterAttributesType: @@ -83,6 +85,7 @@ def collect_filter_attributes(self) -> FilterAttributesType: "filter_code": self.content, "filter_func": self.options.get("filter-func"), "export_id": self.options.get("export_id", ""), + "filter_warning": self.options.get("filter_warning"), } return collected_filter_options diff --git a/tests/doc_test/filter_doc/conf.py b/tests/doc_test/filter_doc/conf.py index ffcff9833..717640d49 100644 --- a/tests/doc_test/filter_doc/conf.py +++ b/tests/doc_test/filter_doc/conf.py @@ -1,4 +1,9 @@ -extensions = ["sphinx_needs"] +import os + +extensions = ["sphinx_needs", "sphinxcontrib.plantuml"] + +# note, the plantuml executable command is set globally in the test suite +plantuml_output_format = "svg" needs_id_regex = "^[A-Za-z0-9_]" @@ -8,4 +13,20 @@ {"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"}, + {"directive": "user", "title": "User", "prefix": "U_", "color": "#777777", "style": "node"}, + {"directive": "action", "title": "Action", "prefix": "A_", "color": "#FFCC00", "style": "node"}, ] + +needs_extra_links = [ + { + "option": "triggers", + "incoming": "triggered by", + "outgoing": "triggers", + "copy": False, + "style": "#00AA00", + "style_part": "solid,#777777", + "allow_dead_links": True, + }, +] + +needs_css = os.path.join(os.path.dirname(__file__), "filter.css") diff --git a/tests/doc_test/filter_doc/filter.css b/tests/doc_test/filter_doc/filter.css new file mode 100644 index 000000000..c611403b1 --- /dev/null +++ b/tests/doc_test/filter_doc/filter.css @@ -0,0 +1,5 @@ + +p.needs_filter_warning { + background-color: grey; + font-weight: bolder; +} diff --git a/tests/doc_test/filter_doc/filter_no_needs.rst b/tests/doc_test/filter_doc/filter_no_needs.rst new file mode 100644 index 000000000..8cb606e82 --- /dev/null +++ b/tests/doc_test/filter_doc/filter_no_needs.rst @@ -0,0 +1,214 @@ +filter_no_needs +=============== + +.. req:: filter_warning_req_a + :id: FILTER_001 + :tags: 1; + :status: open + :hide: + +.. req:: filter_warning_req_b + :id: FILTER_002 + :tags: 2; + :status: closed + :hide: + + +Testing tables +------------------------- + +Should show default message + +.. needtable:: + :filter: ("5" in tags) + +Should show specific message + +.. needtable:: + :filter: ("6" in tags) + :filter_warning: got filter warning from needtable + +Should show no specific message and no default message + +.. needtable:: + :filter: ("7" in tags) + :filter_warning: + +Should show no specific message cause needs found + +.. needtable:: + :filter: ("2" in tags) + :filter_warning: no filter warning from needtable + + +Testing Lists +------------------------- + +Should show specific message + +.. needlist:: + :tags: 7 + :filter_warning: got filter warning from needlist + + +Should show default message + +.. needlist:: + :tags: 7 + +Should show no specific message and no default message + +.. needlist:: + :tags: 7 + :filter_warning: + +Should show no specific message cause needs found + +.. needlist:: + :tags: 1 + :filter_warning: no filter warning from needlist + + +Testing Flows +------------------------- + +Should show specific message + +.. needflow:: + :filter: ("7" in tags) + :filter_warning: got filter warning from needflow + +Should show default message + +.. needflow:: + :filter: ("7" in tags) + + +Should show no specific message and no default message + +.. needlist:: + :filter: ("7" in tags) + :filter_warning: + +Should show no specific message cause needs found + +.. needflow:: + :filter: ("1" in tags) + :filter_warning: no filter warning from needflow + + +Testing Gantt +------------------------- + +Should show specific message + +.. needgantt:: + :tags: 7 + :filter_warning: got filter warning from needgant + +Should show default message + +.. needgantt:: + :tags: 7 + +Should show no specific message and no default message + +.. needgantt:: + :tags: 7 + :filter_warning: + + +Should show no specific message cause needs found + +.. needgantt:: + :tags: 1 + :filter_warning: no filter warning from needgant + +Testing Sequence +------------------------- + +.. user:: User A + :id: USER_A + :links: ACT_ISSUE + :style: blue_border + +.. action:: Creates issue + :id: ACT_ISSUE + :links: USER_B + :style: yellow_border + +.. user:: User B + :id: USER_B + :style: blue_border + +Should show specific message + +.. needsequence:: My filtered sequence + :start: USER_A, USER_B + :link_types: links, triggers + :filter: ("User" not in title) + :filter_warning: got filter warning from needsequence + +Should show default message + +.. needsequence:: My filtered sequence + :start: USER_A, USER_B + :link_types: links, triggers + :filter: ("User" not in title) + + +Should show no specific message and no default message + +.. needsequence:: My filtered sequence + :start: USER_A, USER_B + :link_types: links, triggers + :filter: ("User" not in title) + :filter_warning: + +Should show no specific message cause needs found + +.. needsequence:: My nonfiltered sequence + :start: USER_A, USER_B + :link_types: links, triggers + :filter_warning: no filter warning from needsequence + +Testing Pie +------------------------- + +Should show specific message + +.. needpie:: Empty Pie 0 + :labels: Running, Others + :filter_warning: got filter warning from needpie + + '7' in tags + '9' in tags + +Should show default message + +.. needpie:: Empty Pie 1 + :labels: Running, Others + + '7' in tags + '9' in tags + +Should show no specific message and no default message + +.. needpie:: Empty Pie 2 + :labels: Running, Others + :filter_warning: + + '7' in tags + '9' in tags + +Should show no specific message cause needs found + +.. needpie:: Success Pie + :labels: Open, Closed, Others + :filter_warning: no filter warning from needpie + + 10 + 20 + 30 + + diff --git a/tests/doc_test/filter_doc/index.rst b/tests/doc_test/filter_doc/index.rst index 9a9122fda..ce43a9ff3 100644 --- a/tests/doc_test/filter_doc/index.rst +++ b/tests/doc_test/filter_doc/index.rst @@ -7,6 +7,7 @@ TEST DOCUMENT filter_all filter_search nested_needs + filter_no_needs Testing simple filter --------------------- diff --git a/tests/test_filter.py b/tests/test_filter.py index 8102d8f7f..645e6cb83 100644 --- a/tests/test_filter.py +++ b/tests/test_filter.py @@ -52,3 +52,32 @@ def test_filter_build_html(test_app): '
parent needs: CHILD_1_STORY
' in html_5 ) + + html_6 = Path(app.outdir, "filter_no_needs.html").read_text() + assert html_6.count("No needs passed the filters") == 6 + assert html_6.count("Should show no specific message and no default message") == 6 + assert html_6.count("