Skip to content

Commit 73b961e

Browse files
authored
#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.
1 parent c27bc64 commit 73b961e

18 files changed

+375
-79
lines changed

docs/filter.rst

+19
Original file line numberDiff line numberDiff line change
@@ -453,3 +453,22 @@ Example:
453453
454454
results.append(cnt_x)
455455
results.append(cnt_y)
456+
457+
Filter matches nothing
458+
----------------------
459+
460+
Depending on the directive used a filter that matches no needs may add text to inform that no needs are found.
461+
462+
The default text "No needs passed the filter".
463+
464+
If this is not intended, add the option
465+
466+
.. _option_filter_warning:
467+
468+
filter_warning
469+
~~~~~~~~~~~~~~
470+
471+
Add specific text with this option or add no text to display nothing. The default text will not be shown.
472+
473+
The specified output could be styled with the css class ``needs_filter_warning``
474+

pyproject.toml

+5
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,11 @@ module = [
151151
]
152152
disable_error_code = ["attr-defined", "no-any-return"]
153153

154+
[[tool.mypy.overrides]]
155+
module = [
156+
"sphinx_needs.directives.needextract",
157+
]
158+
disable_error_code = "no-untyped-call"
154159

155160
[build-system]
156161
requires = ["setuptools", "poetry_core>=1.0.8"] # setuptools for deps like plantuml

sphinx_needs/data.py

+2
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,7 @@ class NeedsFilteredBaseType(NeedsBaseDataType):
266266
filter_code: list[str]
267267
filter_func: None | str
268268
export_id: str
269+
filter_warning: str
269270
"""If set, the filter is exported with this ID in the needs.json file."""
270271

271272

@@ -346,6 +347,7 @@ class NeedsPieType(NeedsBaseDataType):
346347
text_color: None | str
347348
shadow: bool
348349
filter_func: None | str
350+
filter_warning: str
349351

350352

351353
class NeedsSequenceType(NeedsFilteredDiagramBaseType):

sphinx_needs/directives/needextract.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ def process_needextract(
120120
content.append(need_extract)
121121

122122
if len(content) == 0:
123-
content.append(no_needs_found_paragraph())
123+
content.append(no_needs_found_paragraph(current_needextract.get("filter_warning")))
124124

125125
if current_needextract["show_filters"]:
126126
content.append(used_filter_paragraph(current_needextract))

sphinx_needs/directives/needfilter.py

+2-5
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from sphinx_needs.config import NeedsSphinxConfig
1212
from sphinx_needs.data import SphinxNeedsData
1313
from sphinx_needs.diagrams_common import create_legend
14+
from sphinx_needs.directives.utils import no_needs_found_paragraph
1415
from sphinx_needs.filter_common import FilterBase, process_filters
1516
from sphinx_needs.utils import add_doc, remove_node_from_tree, row_col_maker
1617

@@ -228,11 +229,7 @@ def process_needfilters(
228229
content.append(puml_node)
229230

230231
if len(content) == 0:
231-
nothing_found = "No needs passed the filters"
232-
para = nodes.line()
233-
nothing_found_node = nodes.Text(nothing_found)
234-
para += nothing_found_node
235-
content.append(para)
232+
content.append(no_needs_found_paragraph(current_needfilter.get("filter_warning")))
236233
if current_needfilter["show_filters"]:
237234
para = nodes.paragraph()
238235
filter_text = "Used filter:"

sphinx_needs/directives/needflow.py

+2-5
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
)
2020
from sphinx_needs.debug import measure_time
2121
from sphinx_needs.diagrams_common import calculate_link, create_legend
22+
from sphinx_needs.directives.utils import no_needs_found_paragraph
2223
from sphinx_needs.filter_common import FilterBase, filter_single_need, process_filters
2324
from sphinx_needs.logging import get_logger
2425
from sphinx_needs.utils import (
@@ -457,11 +458,7 @@ def process_needflow(app: Sphinx, doctree: nodes.document, fromdocname: str, fou
457458

458459
content.append(puml_node)
459460
else: # no needs found
460-
nothing_found = "No needs passed the filters"
461-
para = nodes.paragraph()
462-
nothing_found_node = nodes.Text(nothing_found)
463-
para += nothing_found_node
464-
content.append(para)
461+
content.append(no_needs_found_paragraph(current_needflow.get("filter_warning")))
465462

466463
if current_needflow["show_filters"]:
467464
para = nodes.paragraph()

sphinx_needs/directives/needgantt.py

+6-7
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@
2020
get_filter_para,
2121
no_plantuml,
2222
)
23-
from sphinx_needs.directives.utils import SphinxNeedsLinkTypeException
23+
from sphinx_needs.directives.utils import (
24+
SphinxNeedsLinkTypeException,
25+
no_needs_found_paragraph,
26+
)
2427
from sphinx_needs.filter_common import FilterBase, filter_single_need, process_filters
2528
from sphinx_needs.logging import get_logger
2629
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
312315

313316
content.append(puml_node)
314317

315-
if len(content) == 0:
316-
nothing_found = "No needs passed the filters"
317-
para = nodes.paragraph()
318-
nothing_found_node = nodes.Text(nothing_found)
319-
para += nothing_found_node
320-
content.append(para)
318+
if len(found_needs) == 0:
319+
content = [no_needs_found_paragraph(current_needgantt.get("filter_warning"))]
321320
if current_needgantt["show_filters"]:
322321
content.append(get_filter_para(current_needgantt))
323322

sphinx_needs/directives/needlist.py

+40-40
Original file line numberDiff line numberDiff line change
@@ -83,48 +83,48 @@ def process_needlist(app: Sphinx, doctree: nodes.document, fromdocname: str, fou
8383
all_needs = list(SphinxNeedsData(env).get_or_create_needs().values())
8484
found_needs = process_filters(app, all_needs, current_needfilter)
8585

86-
line_block = nodes.line_block()
87-
88-
# Add lineno to node
89-
line_block.line = current_needfilter["lineno"]
90-
for need_info in found_needs:
91-
para = nodes.line()
92-
description = "{}: {}".format(need_info["id"], need_info["title"])
93-
94-
if current_needfilter["show_status"] and need_info["status"]:
95-
description += " (%s)" % need_info["status"]
96-
97-
if current_needfilter["show_tags"] and need_info["tags"]:
98-
description += " [%s]" % "; ".join(need_info["tags"])
99-
100-
title = nodes.Text(description)
101-
102-
# Create a reference
103-
if need_info["hide"]:
104-
para += title
105-
elif need_info["is_external"]:
106-
assert need_info["external_url"] is not None, "External need without URL"
107-
ref = nodes.reference("", "")
108-
109-
ref["refuri"] = check_and_calc_base_url_rel_path(need_info["external_url"], fromdocname)
110-
111-
ref["classes"].append(need_info["external_css"])
112-
ref.append(title)
113-
para += ref
114-
else:
115-
target_id = need_info["target_id"]
116-
ref = nodes.reference("", "")
117-
ref["refdocname"] = need_info["docname"]
118-
ref["refuri"] = builder.get_relative_uri(fromdocname, need_info["docname"])
119-
ref["refuri"] += "#" + target_id
120-
ref.append(title)
121-
para += ref
122-
line_block.append(para)
123-
content.append(line_block)
86+
if 0 < len(found_needs):
87+
line_block = nodes.line_block()
88+
89+
# Add lineno to node
90+
line_block.line = current_needfilter["lineno"]
91+
for need_info in found_needs:
92+
para = nodes.line()
93+
description = "{}: {}".format(need_info["id"], need_info["title"])
94+
95+
if current_needfilter["show_status"] and need_info["status"]:
96+
description += " (%s)" % need_info["status"]
97+
98+
if current_needfilter["show_tags"] and need_info["tags"]:
99+
description += " [%s]" % "; ".join(need_info["tags"])
100+
101+
title = nodes.Text(description)
102+
103+
# Create a reference
104+
if need_info["hide"]:
105+
para += title
106+
elif need_info["is_external"]:
107+
assert need_info["external_url"] is not None, "External need without URL"
108+
ref = nodes.reference("", "")
109+
110+
ref["refuri"] = check_and_calc_base_url_rel_path(need_info["external_url"], fromdocname)
111+
112+
ref["classes"].append(need_info["external_css"])
113+
ref.append(title)
114+
para += ref
115+
else:
116+
target_id = need_info["target_id"]
117+
ref = nodes.reference("", "")
118+
ref["refdocname"] = need_info["docname"]
119+
ref["refuri"] = builder.get_relative_uri(fromdocname, need_info["docname"])
120+
ref["refuri"] += "#" + target_id
121+
ref.append(title)
122+
para += ref
123+
line_block.append(para)
124+
content.append(line_block)
124125

125126
if len(content) == 0:
126-
content.append(no_needs_found_paragraph())
127-
127+
content.append(no_needs_found_paragraph(current_needfilter.get("filter_warning")))
128128
if current_needfilter["show_filters"]:
129129
content.append(used_filter_paragraph(current_needfilter))
130130

sphinx_needs/directives/needpie.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from sphinx_needs.config import NeedsSphinxConfig
99
from sphinx_needs.data import SphinxNeedsData
1010
from sphinx_needs.debug import measure_time
11+
from sphinx_needs.directives.utils import no_needs_found_paragraph
1112
from sphinx_needs.filter_common import FilterBase, filter_needs, prepare_need_list
1213
from sphinx_needs.logging import get_logger
1314
from sphinx_needs.utils import (
@@ -47,6 +48,7 @@ class NeedpieDirective(FilterBase):
4748
"text_color": directives.unchanged_required,
4849
"shadow": directives.flag,
4950
"filter-func": FilterBase.base_option_spec["filter-func"],
51+
"filter_warning": FilterBase.base_option_spec["filter_warning"],
5052
}
5153

5254
# Update the options_spec only with value filter-func defined in the FilterBase class
@@ -94,6 +96,7 @@ def run(self) -> Sequence[nodes.Node]:
9496
"shadow": shadow,
9597
"text_color": text_color,
9698
"filter_func": self.collect_filter_attributes()["filter_func"],
99+
"filter_warning": self.collect_filter_attributes()["filter_warning"],
97100
}
98101
add_doc(env, env.docname)
99102

@@ -273,7 +276,10 @@ def process_needpie(app: Sphinx, doctree: nodes.document, fromdocname: str, foun
273276
# Add lineno to node
274277
image_node.line = current_needpie["lineno"]
275278

276-
node.replace_self(image_node)
279+
if len(sizes) == 0 or all(s == 0 for s in sizes):
280+
node.replace_self(no_needs_found_paragraph(current_needpie.get("filter_warning")))
281+
else:
282+
node.replace_self(image_node)
277283

278284
# Cleanup matplotlib
279285
# Reset the style configuration:

sphinx_needs/directives/needsequence.py

+3-6
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
get_filter_para,
2020
no_plantuml,
2121
)
22+
from sphinx_needs.directives.utils import no_needs_found_paragraph
2223
from sphinx_needs.filter_common import FilterBase
2324
from sphinx_needs.logging import get_logger
2425
from sphinx_needs.utils import add_doc, remove_node_from_tree
@@ -209,12 +210,8 @@ def process_needsequence(
209210

210211
content.append(puml_node)
211212

212-
if len(content) == 0:
213-
nothing_found = "No needs passed the filters"
214-
para = nodes.paragraph()
215-
nothing_found_node = nodes.Text(nothing_found)
216-
para += nothing_found_node
217-
content.append(para)
213+
if len(c_string) == 0 and p_string.count("participant") == 1: # no connections and just one (start) participant
214+
content = [(no_needs_found_paragraph(current_needsequence.get("filter_warning")))]
218215
if current_needsequence["show_filters"]:
219216
content.append(get_filter_para(current_needsequence))
220217

sphinx_needs/directives/needtable.py

+10-10
Original file line numberDiff line numberDiff line change
@@ -318,8 +318,15 @@ def sort(need: NeedsInfoType) -> Any:
318318
tbody += row
319319

320320
if len(filtered_needs) == 0:
321-
table_node.append(no_needs_found_paragraph())
322-
321+
content = no_needs_found_paragraph(current_needtable.get("filter_warning"))
322+
else:
323+
# Put the table in a div-wrapper, so that we can control overflow / scroll layout
324+
if style == "TABLE":
325+
table_wrapper = nodes.container(classes=["needstable_wrapper"])
326+
table_wrapper.insert(0, table_node)
327+
content = table_wrapper
328+
else:
329+
content = table_node
323330
# add filter information to output
324331
if current_needtable["show_filters"]:
325332
table_node.append(used_filter_paragraph(current_needtable))
@@ -329,11 +336,4 @@ def sort(need: NeedsInfoType) -> Any:
329336
title = nodes.title(title_text, "", nodes.Text(title_text))
330337
table_node.insert(0, title)
331338

332-
# Put the table in a div-wrapper, so that we can control overflow / scroll layout
333-
if style == "TABLE":
334-
table_wrapper = nodes.container(classes=["needstable_wrapper"])
335-
table_wrapper.insert(0, table_node)
336-
node.replace_self(table_wrapper)
337-
338-
else:
339-
node.replace_self(table_node)
339+
node.replace_self(content)

sphinx_needs/directives/utils.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import re
2-
from typing import Any, Dict, List, Tuple
2+
from typing import Any, Dict, List, Optional, Tuple
33

44
from docutils import nodes
55
from sphinx.environment import BuildEnvironment
@@ -9,9 +9,10 @@
99
from sphinx_needs.defaults import TITLE_REGEX
1010

1111

12-
def no_needs_found_paragraph() -> nodes.paragraph:
13-
nothing_found = "No needs passed the filters"
12+
def no_needs_found_paragraph(message: Optional[str]) -> nodes.paragraph:
13+
nothing_found = "No needs passed the filters" if message is None else message
1414
para = nodes.paragraph()
15+
para["classes"].append("needs_filter_warning")
1516
nothing_found_node = nodes.Text(nothing_found)
1617
para += nothing_found_node
1718
return para

sphinx_needs/filter_common.py

+3
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class FilterAttributesType(TypedDict):
3434
filter_code: list[str]
3535
filter_func: str
3636
export_id: str
37+
filter_warning: str
3738
"""If set, the filter is exported with this ID in the needs.json file."""
3839

3940

@@ -48,6 +49,7 @@ class FilterBase(SphinxDirective):
4849
"filter-func": directives.unchanged_required,
4950
"sort_by": directives.unchanged,
5051
"export_id": directives.unchanged,
52+
"filter_warning": directives.unchanged,
5153
}
5254

5355
def collect_filter_attributes(self) -> FilterAttributesType:
@@ -83,6 +85,7 @@ def collect_filter_attributes(self) -> FilterAttributesType:
8385
"filter_code": self.content,
8486
"filter_func": self.options.get("filter-func"),
8587
"export_id": self.options.get("export_id", ""),
88+
"filter_warning": self.options.get("filter_warning"),
8689
}
8790
return collected_filter_options
8891

tests/doc_test/filter_doc/conf.py

+22-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
extensions = ["sphinx_needs"]
1+
import os
2+
3+
extensions = ["sphinx_needs", "sphinxcontrib.plantuml"]
4+
5+
# note, the plantuml executable command is set globally in the test suite
6+
plantuml_output_format = "svg"
27

38
needs_id_regex = "^[A-Za-z0-9_]"
49

@@ -8,4 +13,20 @@
813
{"directive": "spec", "title": "Specification", "prefix": "SP_", "color": "#FEDCD2", "style": "node"},
914
{"directive": "impl", "title": "Implementation", "prefix": "IM_", "color": "#DF744A", "style": "node"},
1015
{"directive": "test", "title": "Test Case", "prefix": "TC_", "color": "#DCB239", "style": "node"},
16+
{"directive": "user", "title": "User", "prefix": "U_", "color": "#777777", "style": "node"},
17+
{"directive": "action", "title": "Action", "prefix": "A_", "color": "#FFCC00", "style": "node"},
1118
]
19+
20+
needs_extra_links = [
21+
{
22+
"option": "triggers",
23+
"incoming": "triggered by",
24+
"outgoing": "triggers",
25+
"copy": False,
26+
"style": "#00AA00",
27+
"style_part": "solid,#777777",
28+
"allow_dead_links": True,
29+
},
30+
]
31+
32+
needs_css = os.path.join(os.path.dirname(__file__), "filter.css")

tests/doc_test/filter_doc/filter.css

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
2+
p.needs_filter_warning {
3+
background-color: grey;
4+
font-weight: bolder;
5+
}

0 commit comments

Comments
 (0)