Skip to content

Commit

Permalink
add deprecations and update documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisjsewell committed Sep 17, 2024
1 parent a3c642c commit 8c52e74
Show file tree
Hide file tree
Showing 14 changed files with 108 additions and 47 deletions.
4 changes: 2 additions & 2 deletions docs/directives/needextend.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ Also, you can add links or delete tags.

This requirement got modified.

| Status was **open**, now it is **[[copy('status')]]**.
| Also author got changed from **Foo** to **[[copy('author')]]**.
| Status was **open**, now it is :ndf:`copy('status')`.
| Also author got changed from **Foo** to :ndf:`copy('author')`.
| And a tag was added.
| Finally all links got removed.
Expand Down
25 changes: 14 additions & 11 deletions docs/dynamic_functions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,35 @@
Dynamic functions
=================

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

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**.

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

**needtable**

The options :ref:`needtable_style_row` of :ref:`needtable` also support
dynamic function execution. In this case, the function gets executed with the found need for each row.

This allows you to set row and column specific styles such as, set a row background to red, if a need-status is *failed*.
To refer to a dynamic function, you can use the following syntax:

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

.. need-example:: Dynamic function example

.. req:: my test requirement
:id: df_1
:status: open
:tags: test;[[copy("status")]]

This need has id **[[copy("id")]]** and status **[[copy("status")]]**.
This need has id :ndf:`copy("id")` and status :ndf:`copy("status")`.

.. deprecated:: 3.1.0

The :ref:`ndf` role replaces the use of the ``[[...]]`` syntax in need content.

Built-in functions
-------------------
The following functions are available in all **Sphinx-Needs** installations.

The following functions are available by default.

.. note::

Expand Down
2 changes: 1 addition & 1 deletion docs/needs_templates/spec_template.need
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,5 @@ Tags:
{# by using dynamic_functions #}
Links:
{% for link in links %}
| **{{link}}**: [[copy('title', '{{link}}')]] ([[copy('type_name', '{{link}}')]])
| **{{link}}**: :ndf:`copy('title', '{{link}}')` (:ndf:`copy('type_name', '{{link}}')`)
{%- endfor %}
14 changes: 11 additions & 3 deletions docs/roles.rst
Original file line number Diff line number Diff line change
Expand Up @@ -188,10 +188,18 @@ To calculate the ratio of one filter to another filter, you can define two filte

need_func
---------
.. versionadded:: 0.6.3
.. deprecated:: 3.1.0

Executes :ref:`dynamic_functions` and uses the return values as content.
Use :ref:`ndf` instead.

.. _ndf:

ndf
---
.. versionadded:: 3.1.0

Executes a :ref:`need dynamic function <dynamic_functions>` and uses the return values as content.

.. need-example::

A nice :need_func:`[[echo("first")]] test` for need_func.
A nice :ndf:`echo("first test")` for dynamic functions.
5 changes: 4 additions & 1 deletion sphinx_needs/directives/needextend.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,10 @@ def run(self) -> Sequence[nodes.Node]:

add_doc(env, env.docname)

return [targetnode, Needextend("")]
node = Needextend("")
self.set_source_info(node)

return [targetnode, node]


RE_ID_FUNC = re.compile(r"\s*((?P<function>\[\[[^\]]*\]\])|(?P<id>[^;,]+))\s*([;,]|$)")
Expand Down
3 changes: 2 additions & 1 deletion sphinx_needs/directives/needextract.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,14 +196,15 @@ def _build_needextract(

dummy_need.extend(need_node.children)

find_and_replace_node_content(dummy_need, env, need_data)

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

dummy_need.attributes["ids"].append(need_data["id"])
find_and_replace_node_content(dummy_need, env, need_data)
rendered_node = build_need_repr(
dummy_need, # type: ignore[arg-type]
need_data,
Expand Down
14 changes: 7 additions & 7 deletions sphinx_needs/functions/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,16 @@ def test(
.. req:: test requirement
[[test('arg_1', [1,2,3], my_keyword='awesome')]]
:ndf:`test('arg_1', [1,2,3], my_keyword='awesome')`
.. req:: test requirement
[[test('arg_1', [1,2,3], my_keyword='awesome')]]
:ndf:`test('arg_1', [1,2,3], my_keyword='awesome')`
:return: single test string
"""
need_id = "none" if need is None else need["id"]
return f"Test output of need_func; need: {need_id}; args: {args}; kwargs: {kwargs}"
return f"Test output of dynamic function; need: {need_id}; args: {args}; kwargs: {kwargs}"


def echo(
Expand All @@ -67,9 +67,9 @@ def echo(
.. code-block:: jinja
A nice :need_func:`[[echo("first")]] test` for need_func.
A nice :ndf:`echo("first test")` for a dynamic function.
**Result**: A nice :need_func:`[[echo("first")]] test` for need_func.
**Result**: A nice :ndf:`echo("first test")` for a dynamic function.
"""
return text
Expand Down Expand Up @@ -146,15 +146,15 @@ def copy(
The following copy command copies the title of the first need found under the same highest
section (headline):
[[copy('title', filter='current_need["sections"][-1]==sections[-1]')]]
:ndf:`copy('title', filter='current_need["sections"][-1]==sections[-1]')`
.. test:: test of current_need value
:id: copy_4
The following copy command copies the title of the first need found under the same highest
section (headline):
[[copy('title', filter='current_need["sections"][-1]==sections[-1]')]]
:ndf:`copy('title', filter='current_need["sections"][-1]==sections[-1]')`
This filter possibilities get really powerful in combination with :ref:`needs_global_options`.
Expand Down
15 changes: 11 additions & 4 deletions sphinx_needs/functions/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ def execute_func(
return func_return


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


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

func_string = func_string.replace("‘", "'") # noqa: RUF001
func_string = func_string.replace("’", "'") # noqa: RUF001

msg = f"The [[{func_string}]] syntax in need content is deprecated. Replace with :ndf:`{func_string}` instead."
log_warning(logger, msg, "deprecation", location=node)

func_return = execute_func(env.app, need, func_string, node)

if isinstance(func_return, list):
Expand Down Expand Up @@ -388,7 +395,7 @@ def check_and_get_content(
:param location: source location of the function call
:return: string
"""
func_match = func_pattern.search(content)
func_match = FUNC_RE.search(content)
if func_match is None:
return content

Expand Down Expand Up @@ -416,7 +423,7 @@ def _detect_and_execute_field(
except UnicodeEncodeError:
content = content.encode("utf-8")

func_match = func_pattern.search(content)
func_match = FUNC_RE.search(content)
if func_match is None:
return None, None

Expand Down
4 changes: 2 additions & 2 deletions sphinx_needs/needs.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,8 +254,8 @@ def setup(app: Sphinx) -> dict[str, Any]:
),
)

app.add_role("need_func", NeedFuncRole())
app.add_role("ndf", NeedFuncRole(no_braces=True))
app.add_role("need_func", NeedFuncRole(with_brackets=True)) # deprecrated
app.add_role("ndf", NeedFuncRole(with_brackets=False))

########################################################################
# EVENTS
Expand Down
32 changes: 24 additions & 8 deletions sphinx_needs/roles/need_func.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
Provide the role ``need_func``, which executes a dynamic function.
Provide a role which executes a dynamic function.
"""

from __future__ import annotations
Expand All @@ -10,48 +10,64 @@
from sphinx.util.docutils import SphinxRole

from sphinx_needs.data import NeedsInfoType
from sphinx_needs.logging import get_logger
from sphinx_needs.logging import get_logger, log_warning
from sphinx_needs.utils import add_doc

log = get_logger(__name__)
LOGGER = get_logger(__name__)


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

def __init__(self, no_braces: bool = False) -> None:
def __init__(self, *, with_brackets: bool = False) -> None:
"""Initialize the role.
:param no_braces: If True, the function should not be wrapped ``[[]]``.
:param with_brackets: If True, the function is expected to be wrapped in brackets ``[[]]``.
"""
self.no_braces = no_braces
self.with_brackets = with_brackets
super().__init__()

def run(self) -> tuple[list[nodes.Node], list[nodes.system_message]]:
add_doc(self.env, self.env.docname)
node = NeedFunc(
self.rawtext,
nodes.literal(self.rawtext, self.text),
no_braces=self.no_braces,
with_brackets=self.with_brackets,
**self.options,
)
self.set_source_info(node)
if self.with_brackets:
from sphinx_needs.functions.functions import FUNC_RE

msg = "The `need_func` role is deprecated. "
if func_match := FUNC_RE.search(node.astext()):
func_call = func_match.group(1)
msg += f"Replace with :ndf:`{func_call}` instead."
else:
msg += "Replace with ndf role instead."

Check warning on line 47 in sphinx_needs/roles/need_func.py

View check run for this annotation

Codecov / codecov/patch

sphinx_needs/roles/need_func.py#L47

Added line #L47 was not covered by tests
log_warning(LOGGER, msg, "deprecation", location=node)
return [node], []


class NeedFunc(nodes.Inline, nodes.Element):
@property
def with_brackets(self) -> bool:
"""Return the function with brackets."""
return self.get("with_brackets", False) # type: ignore[no-any-return]

def get_text(self, env: BuildEnvironment, need: NeedsInfoType | None) -> nodes.Text:
"""Execute function and return result."""
from sphinx_needs.functions.functions import check_and_get_content, execute_func

if self.get("no_braces"):
if not self.with_brackets:
func_return = execute_func(env.app, need, self.astext(), self)
if isinstance(func_return, list):
func_return = ", ".join(str(el) for el in func_return)

Check warning on line 65 in sphinx_needs/roles/need_func.py

View check run for this annotation

Codecov / codecov/patch

sphinx_needs/roles/need_func.py#L65

Added line #L65 was not covered by tests

return nodes.Text("" if func_return is None else str(func_return))

result = check_and_get_content(self.astext(), need, env, self)

return nodes.Text(str(result))


Expand Down
4 changes: 2 additions & 2 deletions tests/doc_test/needextract_with_nested_needs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Test

Another, child spec

This is id [[copy("id")]]
This is id [[copy("id")]] :ndf:`copy("id")`

.. spec:: Child spec
:id: SPEC_1_1
Expand All @@ -30,6 +30,6 @@ Test

awesome grandchild spec number 2.

This is grandchild id [[copy("id")]]
This is grandchild id [[copy("id")]] :ndf:`copy("id")`

Some parent text
13 changes: 12 additions & 1 deletion tests/test_dynamic_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ def test_doc_dynamic_functions(test_app):
app._warning.getvalue().replace(str(app.srcdir) + os.sep, "srcdir/")
).splitlines()
assert warnings == [
'srcdir/index.rst:11: WARNING: The `need_func` role is deprecated. Replace with :ndf:`copy("id")` instead. [needs.deprecation]',
'srcdir/index.rst:40: WARNING: The `need_func` role is deprecated. Replace with :ndf:`copy("id")` instead. [needs.deprecation]',
'srcdir/index.rst:44: WARNING: The `need_func` role is deprecated. Replace with :ndf:`copy("id")` instead. [needs.deprecation]',
'srcdir/index.rst:9: WARNING: The [[copy("id")]] syntax in need content is deprecated. Replace with :ndf:`copy("id")` instead. [needs.deprecation]',
'srcdir/index.rst:27: WARNING: The [[copy("tags")]] syntax in need content is deprecated. Replace with :ndf:`copy("tags")` instead. [needs.deprecation]',
"srcdir/index.rst:33: WARNING: The [[copy('id')]] syntax in need content is deprecated. Replace with :ndf:`copy('id')` instead. [needs.deprecation]",
"srcdir/index.rst:38: WARNING: The [[copy('id')]] syntax in need content is deprecated. Replace with :ndf:`copy('id')` instead. [needs.deprecation]",
"srcdir/index.rst:44: WARNING: Error while executing function 'copy': Need not found [needs.dynamic_function]",
"srcdir/index.rst:44: WARNING: Error while executing function 'copy': Need not found [needs.dynamic_function]",
]
Expand Down Expand Up @@ -61,7 +68,7 @@ def test_doc_dynamic_functions(test_app):
sum(1 for _ in re.finditer('<span class="needs_data">TEST_5</span>', html)) == 2
)

assert "Test output of need_func; need: TEST_3" in html
assert "Test output of dynamic function; need: TEST_3" in html

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

Expand Down Expand Up @@ -121,8 +128,12 @@ def test_doc_df_user_functions(test_app):
# print(warnings)
expected = [
"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]",
"srcdir/index.rst:8: WARNING: The [[my_own_function()]] syntax in need content is deprecated. Replace with :ndf:`my_own_function()` instead. [needs.deprecation]",
"srcdir/index.rst:14: WARNING: The [[bad_function()]] syntax in need content is deprecated. Replace with :ndf:`bad_function()` instead. [needs.deprecation]",
"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]",
"srcdir/index.rst:16: WARNING: The [[invalid]] syntax in need content is deprecated. Replace with :ndf:`invalid` instead. [needs.deprecation]",
"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]",
"srcdir/index.rst:18: WARNING: The [[unknown()]] syntax in need content is deprecated. Replace with :ndf:`unknown()` instead. [needs.deprecation]",
"srcdir/index.rst:18: WARNING: Unknown function 'unknown' [needs.dynamic_function]",
]
if version_info >= (7, 3):
Expand Down
2 changes: 1 addition & 1 deletion tests/test_global_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def test_doc_global_option(test_app):

assert "test_global" in html
assert "1.27" in html
assert "Test output of need_func; need: GLOBAL_ID" in html
assert "Test output of dynamic function; need: GLOBAL_ID" in html

assert "STATUS_IMPL" in html
assert "STATUS_UNKNOWN" in html
Expand Down
Loading

0 comments on commit 8c52e74

Please sign in to comment.