Skip to content

Commit 976dcd5

Browse files
authored
✨ Add ndf role, deprecate need_func & [[...]] in need content (#1269)
There were previously two methods to use dynamic functions within need directive's content: 1. ``[[copy("id")]]``; is problematic, because it does not adhere to the rst / myst syntax specification, and has already shown to cause parsing issues and be surprising to users 2. `` :need_func:`[[copy("id")]]` ``; is better but overly verbose In this commit, these are replaced by the role: `` :ndf:`copy("id")` `` Here we take the entire content to be the function, as so do not require the `[[]]`, reducing verbosity and processing The other two methods now emit a deprecation warning, showing how to convert them to the new role.
1 parent 72138fd commit 976dcd5

File tree

15 files changed

+132
-45
lines changed

15 files changed

+132
-45
lines changed

docs/directives/needextend.rst

+2-2
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ Also, you can add links or delete tags.
3535

3636
This requirement got modified.
3737

38-
| Status was **open**, now it is **[[copy('status')]]**.
39-
| Also author got changed from **Foo** to **[[copy('author')]]**.
38+
| Status was **open**, now it is :ndf:`copy('status')`.
39+
| Also author got changed from **Foo** to :ndf:`copy('author')`.
4040
| And a tag was added.
4141
| Finally all links got removed.
4242

docs/dynamic_functions.rst

+14-11
Original file line numberDiff line numberDiff line change
@@ -3,32 +3,35 @@
33
Dynamic functions
44
=================
55

6-
**Sphinx-Needs** provides a mechanism to set dynamic data for need-options during generation.
7-
We do this by giving an author the possibility to set a function call to a predefined function, which calculates
8-
the final result/value for the option.
6+
Dynamic functions provide a mechanism to specify need fields or content that are calculated at build time, based on other fields or needs.
7+
8+
We do this by giving an author the possibility to set a function call to a predefined function, which calculates the final value **after all needs have been collected**.
99

1010
For instance, you can use the feature if the status of a requirement depends on linked test cases and their status.
1111
Or if you will request specific data from an external server like JIRA.
1212

13-
**needtable**
14-
15-
The options :ref:`needtable_style_row` of :ref:`needtable` also support
16-
dynamic function execution. In this case, the function gets executed with the found need for each row.
17-
18-
This allows you to set row and column specific styles such as, set a row background to red, if a need-status is *failed*.
13+
To refer to a dynamic function, you can use the following syntax:
1914

15+
- In a need directive option, wrap the function call in double square brackets: ``function_name(arg)``
16+
- In a need content, use the :ref:`ndf` role: ``:ndf:\`function_name(arg)\```
2017

2118
.. need-example:: Dynamic function example
2219

2320
.. req:: my test requirement
2421
:id: df_1
2522
:status: open
23+
:tags: test;[[copy("status")]]
2624

27-
This need has id **[[copy("id")]]** and status **[[copy("status")]]**.
25+
This need has id :ndf:`copy("id")` and status :ndf:`copy("status")`.
26+
27+
.. deprecated:: 3.1.0
28+
29+
The :ref:`ndf` role replaces the use of the ``[[...]]`` syntax in need content.
2830

2931
Built-in functions
3032
-------------------
31-
The following functions are available in all **Sphinx-Needs** installations.
33+
34+
The following functions are available by default.
3235

3336
.. note::
3437

docs/needs_templates/spec_template.need

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,5 @@ Tags:
1919
{# by using dynamic_functions #}
2020
Links:
2121
{% for link in links %}
22-
| **{{link}}**: [[copy('title', '{{link}}')]] ([[copy('type_name', '{{link}}')]])
22+
| **{{link}}**: :ndf:`copy('title', '{{link}}')` (:ndf:`copy('type_name', '{{link}}')`)
2323
{%- endfor %}

docs/roles.rst

+11-3
Original file line numberDiff line numberDiff line change
@@ -188,10 +188,18 @@ To calculate the ratio of one filter to another filter, you can define two filte
188188

189189
need_func
190190
---------
191-
.. versionadded:: 0.6.3
191+
.. deprecated:: 3.1.0
192192

193-
Executes :ref:`dynamic_functions` and uses the return values as content.
193+
Use :ref:`ndf` instead.
194+
195+
.. _ndf:
196+
197+
ndf
198+
---
199+
.. versionadded:: 3.1.0
200+
201+
Executes a :ref:`need dynamic function <dynamic_functions>` and uses the return values as content.
194202

195203
.. need-example::
196204

197-
A nice :need_func:`[[echo("first")]] test` for need_func.
205+
A nice :ndf:`echo("first test")` for dynamic functions.

sphinx_needs/directives/needextend.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,10 @@ def run(self) -> Sequence[nodes.Node]:
7171

7272
add_doc(env, env.docname)
7373

74-
return [targetnode, Needextend("")]
74+
node = Needextend("")
75+
self.set_source_info(node)
76+
77+
return [targetnode, node]
7578

7679

7780
RE_ID_FUNC = re.compile(r"\s*((?P<function>\[\[[^\]]*\]\])|(?P<id>[^;,]+))\s*([;,]|$)")

sphinx_needs/directives/needextract.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -196,14 +196,15 @@ def _build_needextract(
196196

197197
dummy_need.extend(need_node.children)
198198

199+
find_and_replace_node_content(dummy_need, env, need_data)
200+
199201
# resolve_references() ignores the given docname and takes the docname from the pending_xref node.
200202
# Therefore, we need to manipulate this first, before we can ask Sphinx to perform the normal
201203
# reference handling for us.
202204
_replace_pending_xref_refdoc(dummy_need, extract_data["docname"])
203205
env.resolve_references(dummy_need, extract_data["docname"], app.builder) # type: ignore[arg-type]
204206

205207
dummy_need.attributes["ids"].append(need_data["id"])
206-
find_and_replace_node_content(dummy_need, env, need_data)
207208
rendered_node = build_need_repr(
208209
dummy_need, # type: ignore[arg-type]
209210
need_data,

sphinx_needs/functions/common.py

+7-7
Original file line numberDiff line numberDiff line change
@@ -39,16 +39,16 @@ def test(
3939
4040
.. req:: test requirement
4141
42-
[[test('arg_1', [1,2,3], my_keyword='awesome')]]
42+
:ndf:`test('arg_1', [1,2,3], my_keyword='awesome')`
4343
4444
.. req:: test requirement
4545
46-
[[test('arg_1', [1,2,3], my_keyword='awesome')]]
46+
:ndf:`test('arg_1', [1,2,3], my_keyword='awesome')`
4747
4848
:return: single test string
4949
"""
5050
need_id = "none" if need is None else need["id"]
51-
return f"Test output of need_func; need: {need_id}; args: {args}; kwargs: {kwargs}"
51+
return f"Test output of dynamic function; need: {need_id}; args: {args}; kwargs: {kwargs}"
5252

5353

5454
def echo(
@@ -67,9 +67,9 @@ def echo(
6767
6868
.. code-block:: jinja
6969
70-
A nice :need_func:`[[echo("first")]] test` for need_func.
70+
A nice :ndf:`echo("first test")` for a dynamic function.
7171
72-
**Result**: A nice :need_func:`[[echo("first")]] test` for need_func.
72+
**Result**: A nice :ndf:`echo("first test")` for a dynamic function.
7373
7474
"""
7575
return text
@@ -146,15 +146,15 @@ def copy(
146146
The following copy command copies the title of the first need found under the same highest
147147
section (headline):
148148
149-
[[copy('title', filter='current_need["sections"][-1]==sections[-1]')]]
149+
:ndf:`copy('title', filter='current_need["sections"][-1]==sections[-1]')`
150150
151151
.. test:: test of current_need value
152152
:id: copy_4
153153
154154
The following copy command copies the title of the first need found under the same highest
155155
section (headline):
156156
157-
[[copy('title', filter='current_need["sections"][-1]==sections[-1]')]]
157+
:ndf:`copy('title', filter='current_need["sections"][-1]==sections[-1]')`
158158
159159
This filter possibilities get really powerful in combination with :ref:`needs_global_options`.
160160

sphinx_needs/functions/functions.py

+11-4
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ def execute_func(
152152
return func_return
153153

154154

155-
func_pattern = re.compile(r"\[\[(.*?)\]\]") # RegEx to detect function strings
155+
FUNC_RE = re.compile(r"\[\[(.*?)\]\]") # RegEx to detect function strings
156156

157157

158158
def find_and_replace_node_content(
@@ -163,6 +163,9 @@ def find_and_replace_node_content(
163163
if found, check if it contains a function string and run/replace it.
164164
165165
:param node: Node to analyse
166+
:param env: Sphinx environment
167+
:param need: Need data
168+
:param extract: If True, the function has been called from a needextract node
166169
"""
167170
new_children = []
168171
if isinstance(node, NeedFunc):
@@ -181,7 +184,7 @@ def find_and_replace_node_content(
181184
return node
182185
else:
183186
new_text = node
184-
func_match = func_pattern.findall(new_text)
187+
func_match = FUNC_RE.findall(new_text)
185188
for func_string in func_match:
186189
# sphinx is replacing ' and " with language specific quotation marks (up and down), which makes
187190
# it impossible for the later used AST render engine to detect a python function call in the given
@@ -194,6 +197,10 @@ def find_and_replace_node_content(
194197

195198
func_string = func_string.replace("‘", "'") # noqa: RUF001
196199
func_string = func_string.replace("’", "'") # noqa: RUF001
200+
201+
msg = f"The [[{func_string}]] syntax in need content is deprecated. Replace with :ndf:`{func_string}` instead."
202+
log_warning(logger, msg, "deprecation", location=node)
203+
197204
func_return = execute_func(env.app, need, func_string, node)
198205

199206
if isinstance(func_return, list):
@@ -388,7 +395,7 @@ def check_and_get_content(
388395
:param location: source location of the function call
389396
:return: string
390397
"""
391-
func_match = func_pattern.search(content)
398+
func_match = FUNC_RE.search(content)
392399
if func_match is None:
393400
return content
394401

@@ -416,7 +423,7 @@ def _detect_and_execute_field(
416423
except UnicodeEncodeError:
417424
content = content.encode("utf-8")
418425

419-
func_match = func_pattern.search(content)
426+
func_match = FUNC_RE.search(content)
420427
if func_match is None:
421428
return None, None
422429

sphinx_needs/needs.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,8 @@ def setup(app: Sphinx) -> dict[str, Any]:
254254
),
255255
)
256256

257-
app.add_role("need_func", NeedFuncRole())
257+
app.add_role("need_func", NeedFuncRole(with_brackets=True)) # deprecrated
258+
app.add_role("ndf", NeedFuncRole(with_brackets=False))
258259

259260
########################################################################
260261
# EVENTS

sphinx_needs/roles/need_func.py

+39-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""
2-
Provide the role ``need_func``, which executes a dynamic function.
2+
Provide a role which executes a dynamic function.
33
"""
44

55
from __future__ import annotations
@@ -10,30 +10,64 @@
1010
from sphinx.util.docutils import SphinxRole
1111

1212
from sphinx_needs.data import NeedsInfoType
13-
from sphinx_needs.logging import get_logger
13+
from sphinx_needs.logging import get_logger, log_warning
1414
from sphinx_needs.utils import add_doc
1515

16-
log = get_logger(__name__)
16+
LOGGER = get_logger(__name__)
1717

1818

1919
class NeedFuncRole(SphinxRole):
2020
"""Role for creating ``NeedFunc`` node."""
2121

22+
def __init__(self, *, with_brackets: bool = False) -> None:
23+
"""Initialize the role.
24+
25+
:param with_brackets: If True, the function is expected to be wrapped in brackets ``[[]]``.
26+
"""
27+
self.with_brackets = with_brackets
28+
super().__init__()
29+
2230
def run(self) -> tuple[list[nodes.Node], list[nodes.system_message]]:
2331
add_doc(self.env, self.env.docname)
2432
node = NeedFunc(
25-
self.rawtext, nodes.literal(self.rawtext, self.text), **self.options
33+
self.rawtext,
34+
nodes.literal(self.rawtext, self.text),
35+
with_brackets=self.with_brackets,
36+
**self.options,
2637
)
2738
self.set_source_info(node)
39+
if self.with_brackets:
40+
from sphinx_needs.functions.functions import FUNC_RE
41+
42+
msg = "The `need_func` role is deprecated. "
43+
if func_match := FUNC_RE.search(node.astext()):
44+
func_call = func_match.group(1)
45+
msg += f"Replace with :ndf:`{func_call}` instead."
46+
else:
47+
msg += "Replace with ndf role instead."
48+
log_warning(LOGGER, msg, "deprecation", location=node)
2849
return [node], []
2950

3051

3152
class NeedFunc(nodes.Inline, nodes.Element):
53+
@property
54+
def with_brackets(self) -> bool:
55+
"""Return the function with brackets."""
56+
return self.get("with_brackets", False) # type: ignore[no-any-return]
57+
3258
def get_text(self, env: BuildEnvironment, need: NeedsInfoType | None) -> nodes.Text:
3359
"""Execute function and return result."""
34-
from sphinx_needs.functions.functions import check_and_get_content
60+
from sphinx_needs.functions.functions import check_and_get_content, execute_func
61+
62+
if not self.with_brackets:
63+
func_return = execute_func(env.app, need, self.astext(), self)
64+
if isinstance(func_return, list):
65+
func_return = ", ".join(str(el) for el in func_return)
66+
67+
return nodes.Text("" if func_return is None else str(func_return))
3568

3669
result = check_and_get_content(self.astext(), need, env, self)
70+
3771
return nodes.Text(str(result))
3872

3973

tests/doc_test/doc_dynamic_functions/index.rst

+5-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ DYNAMIC FUNCTIONS
1010

1111
This is also id :need_func:`[[copy("id")]]`
1212

13+
This is the best id :ndf:`copy("id")`
14+
1315
.. spec:: TEST_2
1416
:id: TEST_2
1517
:tags: my_tag; [[copy("tags", "SP_TOO_001")]]
@@ -37,4 +39,6 @@ DYNAMIC FUNCTIONS
3739

3840
nested id also :need_func:`[[copy("id")]]`
3941

40-
This should warn since it has no associated need: :need_func:`[[copy("id")]]`
42+
nested id best :ndf:`copy("id")`
43+
44+
These should warn since they have no associated need: :need_func:`[[copy("id")]]`, :ndf:`copy("id")`

tests/doc_test/needextract_with_nested_needs/index.rst

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Test
1010

1111
Another, child spec
1212

13-
This is id [[copy("id")]]
13+
This is id [[copy("id")]] :ndf:`copy("id")`
1414

1515
.. spec:: Child spec
1616
:id: SPEC_1_1
@@ -30,6 +30,6 @@ Test
3030

3131
awesome grandchild spec number 2.
3232

33-
This is grandchild id [[copy("id")]]
33+
This is grandchild id [[copy("id")]] :ndf:`copy("id")`
3434

3535
Some parent text

tests/test_dynamic_functions.py

+16-2
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,21 @@ def test_doc_dynamic_functions(test_app):
2626
app._warning.getvalue().replace(str(app.srcdir) + os.sep, "srcdir/")
2727
).splitlines()
2828
assert warnings == [
29-
"srcdir/index.rst:40: WARNING: Error while executing function 'copy': Need not found [needs.dynamic_function]"
29+
'srcdir/index.rst:11: WARNING: The `need_func` role is deprecated. Replace with :ndf:`copy("id")` instead. [needs.deprecation]',
30+
'srcdir/index.rst:40: WARNING: The `need_func` role is deprecated. Replace with :ndf:`copy("id")` instead. [needs.deprecation]',
31+
'srcdir/index.rst:44: WARNING: The `need_func` role is deprecated. Replace with :ndf:`copy("id")` instead. [needs.deprecation]',
32+
'srcdir/index.rst:9: WARNING: The [[copy("id")]] syntax in need content is deprecated. Replace with :ndf:`copy("id")` instead. [needs.deprecation]',
33+
'srcdir/index.rst:27: WARNING: The [[copy("tags")]] syntax in need content is deprecated. Replace with :ndf:`copy("tags")` instead. [needs.deprecation]',
34+
"srcdir/index.rst:33: WARNING: The [[copy('id')]] syntax in need content is deprecated. Replace with :ndf:`copy('id')` instead. [needs.deprecation]",
35+
"srcdir/index.rst:38: WARNING: The [[copy('id')]] syntax in need content is deprecated. Replace with :ndf:`copy('id')` instead. [needs.deprecation]",
36+
"srcdir/index.rst:44: WARNING: Error while executing function 'copy': Need not found [needs.dynamic_function]",
37+
"srcdir/index.rst:44: WARNING: Error while executing function 'copy': Need not found [needs.dynamic_function]",
3038
]
3139

3240
html = Path(app.outdir, "index.html").read_text()
3341
assert "This is id SP_TOO_001" in html
3442
assert "This is also id SP_TOO_001" in html
43+
assert "This is the best id SP_TOO_001" in html
3544

3645
assert (
3746
sum(1 for _ in re.finditer('<span class="needs_data">test2</span>', html)) == 2
@@ -59,14 +68,15 @@ def test_doc_dynamic_functions(test_app):
5968
sum(1 for _ in re.finditer('<span class="needs_data">TEST_5</span>', html)) == 2
6069
)
6170

62-
assert "Test output of need_func; need: TEST_3" in html
71+
assert "Test output of dynamic function; need: TEST_3" in html
6372

6473
assert "Test dynamic func in tags: test_4a, test_4b, TEST_4" in html
6574

6675
assert '<a class="reference external" href="http://www.TEST_5">link</a>' in html
6776

6877
assert "nested id TEST_6" in html
6978
assert "nested id also TEST_6" in html
79+
assert "nested id best TEST_6" in html
7080

7181

7282
@pytest.mark.parametrize(
@@ -118,8 +128,12 @@ def test_doc_df_user_functions(test_app):
118128
# print(warnings)
119129
expected = [
120130
"srcdir/index.rst:10: WARNING: Return value of function 'bad_function' is of type <class 'object'>. Allowed are str, int, float, list [needs.dynamic_function]",
131+
"srcdir/index.rst:8: WARNING: The [[my_own_function()]] syntax in need content is deprecated. Replace with :ndf:`my_own_function()` instead. [needs.deprecation]",
132+
"srcdir/index.rst:14: WARNING: The [[bad_function()]] syntax in need content is deprecated. Replace with :ndf:`bad_function()` instead. [needs.deprecation]",
121133
"srcdir/index.rst:14: WARNING: Return value of function 'bad_function' is of type <class 'object'>. Allowed are str, int, float, list [needs.dynamic_function]",
134+
"srcdir/index.rst:16: WARNING: The [[invalid]] syntax in need content is deprecated. Replace with :ndf:`invalid` instead. [needs.deprecation]",
122135
"srcdir/index.rst:16: WARNING: Function string 'invalid' could not be parsed: Given dynamic function string is not a valid python call. Got: invalid [needs.dynamic_function]",
136+
"srcdir/index.rst:18: WARNING: The [[unknown()]] syntax in need content is deprecated. Replace with :ndf:`unknown()` instead. [needs.deprecation]",
123137
"srcdir/index.rst:18: WARNING: Unknown function 'unknown' [needs.dynamic_function]",
124138
]
125139
if version_info >= (7, 3):

0 commit comments

Comments
 (0)