Skip to content

Commit

Permalink
👌 pass parent need to need_func (#1266)
Browse files Browse the repository at this point in the history
Currently, the `need_func` role passes a "dummy need" to dynamic functions, which would except for most dynamic functions. So:

1. we replace the dummy need with `None` and ensure dynamic functions account for this input.
2. when a `need_func` is used within a need directive, it uses that need as input for the dynamic function, rather than `None`
  • Loading branch information
chrisjsewell authored Sep 6, 2024
1 parent c862e9d commit f3745ff
Show file tree
Hide file tree
Showing 7 changed files with 100 additions and 52 deletions.
44 changes: 30 additions & 14 deletions sphinx_needs/functions/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

def test(
app: Sphinx,
need: NeedsInfoType,
need: NeedsInfoType | None,
needs: NeedsView,
*args: Any,
**kwargs: Any,
Expand All @@ -44,12 +44,13 @@ def test(
:return: single test string
"""
return f"Test output of need {need['id']}. args: {args}. kwargs: {kwargs}"
need_id = "none" if need is None else need["id"]
return f"Test output of need_func; need: {need_id}; args: {args}; kwargs: {kwargs}"


def echo(
app: Sphinx,
need: NeedsInfoType,
need: NeedsInfoType | None,
needs: NeedsView,
text: str,
*args: Any,
Expand All @@ -73,7 +74,7 @@ def echo(

def copy(
app: Sphinx,
need: NeedsInfoType,
need: NeedsInfoType | None,
needs: NeedsView,
option: str,
need_id: str | None = None,
Expand Down Expand Up @@ -139,7 +140,7 @@ def copy(
.. test:: test of current_need value
:id: copy_4
The es the title of the first need found under the same highest
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]')]]
Expand All @@ -150,7 +151,7 @@ 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]')]]
[[copy('title', filter='current_need["sections"][-1]==sections[-1]')]]
This filter possibilities get really powerful in combination with :ref:`needs_global_options`.
Expand All @@ -171,26 +172,32 @@ def copy(
NeedsSphinxConfig(app.config),
filter,
need,
location=(need["docname"], need["lineno"]) if need["docname"] else None,
location=(need["docname"], need["lineno"])
if need and need["docname"]
else None,
)
if result:
need = result[0]

value = need[option] # type: ignore[literal-required]
if need is None:
raise ValueError("Need not found")

if option not in need:
raise ValueError(f"Option {option} not found in need {need['id']}")

# TODO check if str?
value = need[option] # type: ignore[literal-required]

if lower:
return value.lower()
return str(value).lower()
if upper:
return value.upper()
return str(value).upper()

return value


def check_linked_values(
app: Sphinx,
need: NeedsInfoType,
need: NeedsInfoType | None,
needs: NeedsView,
result: Any,
search_option: str,
Expand Down Expand Up @@ -329,6 +336,9 @@ def check_linked_values(
:param one_hit: If True, only one linked need must have a positive check
:return: result, if all checks are positive
"""
if need is None:
raise ValueError("No need given for check_linked_values")

needs_config = NeedsSphinxConfig(app.config)
links = need["links"]
if not isinstance(search_value, list):
Expand Down Expand Up @@ -359,7 +369,7 @@ def check_linked_values(

def calc_sum(
app: Sphinx,
need: NeedsInfoType,
need: NeedsInfoType | None,
needs: NeedsView,
option: str,
filter: str | None = None,
Expand Down Expand Up @@ -444,6 +454,9 @@ def calc_sum(
:return: A float number
"""
if need is None:
raise ValueError("No need given for check_linked_values")

needs_config = NeedsSphinxConfig(app.config)
check_needs = (
[needs[link] for link in need["links"]] if links_only else needs.values()
Expand Down Expand Up @@ -471,7 +484,7 @@ def calc_sum(

def links_from_content(
app: Sphinx,
need: NeedsInfoType,
need: NeedsInfoType | None,
needs: NeedsView,
need_id: str | None = None,
filter: str | None = None,
Expand Down Expand Up @@ -529,6 +542,9 @@ def links_from_content(
"""
source_need = needs[need_id] if need_id else need

if source_need is None:
raise ValueError("No need found for links_from_content")

links = re.findall(r":need:`(\w+)`|:need:`.+\<(.+)\>`", source_need["content"])
raw_links = []
for link in links:
Expand Down
45 changes: 27 additions & 18 deletions sphinx_needs/functions/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from sphinx_needs.debug import measure_time_func
from sphinx_needs.logging import get_logger, log_warning
from sphinx_needs.nodes import Need
from sphinx_needs.roles.need_func import NeedFunc
from sphinx_needs.utils import NEEDS_FUNCTIONS, match_variants

logger = get_logger(__name__)
Expand All @@ -38,7 +39,7 @@ class DynamicFunction(Protocol):
def __call__(
self,
app: Sphinx,
need: NeedsInfoType,
need: NeedsInfoType | None,
needs: NeedsView,
*args: Any,
**kwargs: Any,
Expand Down Expand Up @@ -76,7 +77,7 @@ def register_func(need_function: DynamicFunction, name: str | None = None) -> No

def execute_func(
app: Sphinx,
need: NeedsInfoType,
need: NeedsInfoType | None,
func_string: str,
location: str | tuple[str | None, int | None] | nodes.Node | None,
) -> str | int | float | list[str] | list[int] | list[float] | None:
Expand Down Expand Up @@ -112,13 +113,23 @@ def execute_func(
func = measure_time_func(
NEEDS_FUNCTIONS[func_name]["function"], category="dyn_func", source="user"
)
func_return = func(
app,
need,
SphinxNeedsData(app.env).get_needs_view(),
*func_args,
**func_kwargs,
)

try:
func_return = func(
app,
need,
SphinxNeedsData(app.env).get_needs_view(),
*func_args,
**func_kwargs,
)
except Exception as e:
log_warning(
logger,
f"Error while executing function {func_name!r}: {e}",
"dynamic_function",
location=location,
)
return "??"

if func_return is not None and not isinstance(func_return, (str, int, float, list)):
log_warning(
Expand Down Expand Up @@ -152,10 +163,11 @@ def find_and_replace_node_content(
if found, check if it contains a function string and run/replace it.
:param node: Node to analyse
:return: None
"""
new_children = []
if (
if isinstance(node, NeedFunc):
return node.get_text(env, need)
elif (
not node.children
and isinstance(node, nodes.Text)
or isinstance(node, nodes.reference)
Expand Down Expand Up @@ -360,7 +372,10 @@ def resolve_variants_options(


def check_and_get_content(
content: str, need: NeedsInfoType, env: BuildEnvironment, location: nodes.Node
content: str,
need: NeedsInfoType | None,
env: BuildEnvironment,
location: nodes.Node,
) -> str:
"""
Checks if the given content is a function call.
Expand All @@ -373,12 +388,6 @@ def check_and_get_content(
:param location: source location of the function call
:return: string
"""

try:
content = str(content)
except UnicodeEncodeError:
content = content.encode("utf-8") # type: ignore

func_match = func_pattern.search(content)
if func_match is None:
return content
Expand Down
9 changes: 2 additions & 7 deletions sphinx_needs/needs.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@
from sphinx_needs.logging import get_logger, log_warning
from sphinx_needs.roles import NeedsXRefRole
from sphinx_needs.roles.need_count import NeedCount, process_need_count
from sphinx_needs.roles.need_func import NeedFunc, process_need_func
from sphinx_needs.roles.need_func import NeedFunc, NeedFuncRole, process_need_func
from sphinx_needs.roles.need_incoming import NeedIncoming, process_need_incoming
from sphinx_needs.roles.need_outgoing import NeedOutgoing, process_need_outgoing
from sphinx_needs.roles.need_part import NeedPart, process_need_part
Expand Down Expand Up @@ -253,12 +253,7 @@ def setup(app: Sphinx) -> dict[str, Any]:
),
)

app.add_role(
"need_func",
NeedsXRefRole(
nodeclass=NeedFunc, innernodeclass=nodes.inline, warn_dangling=True
),
)
app.add_role("need_func", NeedFuncRole())

########################################################################
# EVENTS
Expand Down
34 changes: 24 additions & 10 deletions sphinx_needs/roles/need_func.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,35 @@

from docutils import nodes
from sphinx.application import Sphinx
from sphinx.environment import BuildEnvironment
from sphinx.util.docutils import SphinxRole

from sphinx_needs.data import NeedsInfoType
from sphinx_needs.functions.functions import check_and_get_content
from sphinx_needs.logging import get_logger
from sphinx_needs.utils import add_doc

log = get_logger(__name__)


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

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), **self.options
)
self.set_source_info(node)
return [node], []


class NeedFunc(nodes.Inline, nodes.Element):
pass
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

result = check_and_get_content(self.astext(), need, env, self)
return nodes.Text(str(result))


def process_need_func(
Expand All @@ -24,12 +43,7 @@ def process_need_func(
_fromdocname: str,
found_nodes: list[nodes.Element],
) -> None:
env = app.env
# for node_need_func in doctree.findall(NeedFunc):
dummy_need: NeedsInfoType = {"id": "need_func_dummy"} # type: ignore[typeddict-item]
for node_need_func in found_nodes:
result = check_and_get_content(
node_need_func.attributes["reftarget"], dummy_need, env, node_need_func
)
new_node_func = nodes.Text(str(result))
node_need_func: NeedFunc
for node_need_func in found_nodes: # type: ignore[assignment]
new_node_func = node_need_func.get_text(app.env, None)
node_need_func.replace_self(new_node_func)
8 changes: 8 additions & 0 deletions tests/doc_test/doc_dynamic_functions/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ DYNAMIC FUNCTIONS

This is id [[copy("id")]]

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

.. spec:: TEST_2
:id: TEST_2
:tags: my_tag; [[copy("tags", "SP_TOO_001")]]
Expand All @@ -20,6 +22,8 @@ DYNAMIC FUNCTIONS
:id: TEST_4
:tags: test_4a;test_4b;[[copy('title')]]

Test dynamic func in tags: [[copy("tags")]]

.. spec:: TEST_5
:id: TEST_5
:tags: [[copy('id')]]
Expand All @@ -30,3 +34,7 @@ DYNAMIC FUNCTIONS
:id: TEST_6

nested id [[copy('id')]]

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

This should warn since it has no associated need: :need_func:`[[copy("id")]]`
10 changes: 8 additions & 2 deletions tests/test_dynamic_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,13 @@ def test_doc_dynamic_functions(test_app):
warnings = strip_colors(
app._warning.getvalue().replace(str(app.srcdir) + os.sep, "srcdir/")
).splitlines()
assert warnings == []
assert warnings == [
"srcdir/index.rst:40: WARNING: Error while executing function 'copy': Need not found [needs.dynamic_function]"
]

html = Path(app.outdir, "index.html").read_text()
assert "This is id SP_TOO_001" in html
assert "This is also id SP_TOO_001" in html

assert (
sum(1 for _ in re.finditer('<span class="needs_data">test2</span>', html)) == 2
Expand Down Expand Up @@ -56,11 +59,14 @@ 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 TEST_3. args:" in html
assert "Test output of need_func; need: TEST_3" in html

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

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

assert "nested id TEST_6" in html
assert "nested id also TEST_6" in html


@pytest.mark.parametrize(
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 GLOBAL_ID" in html
assert "Test output of need_func; need: GLOBAL_ID" in html

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

0 comments on commit f3745ff

Please sign in to comment.