Skip to content

Commit

Permalink
👌 pass parent need to need_func
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisjsewell committed Sep 3, 2024
1 parent 3916cb7 commit 0d4d18e
Show file tree
Hide file tree
Showing 7 changed files with 104 additions and 50 deletions.
40 changes: 28 additions & 12 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 @@ -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']}")

Check warning on line 186 in sphinx_needs/functions/common.py

View check run for this annotation

Codecov / codecov/patch

sphinx_needs/functions/common.py#L186

Added line #L186 was not covered by tests

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

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

Check warning on line 191 in sphinx_needs/functions/common.py

View check run for this annotation

Codecov / codecov/patch

sphinx_needs/functions/common.py#L191

Added line #L191 was not covered by tests
if upper:
return value.upper()
return str(value).upper()

Check warning on line 193 in sphinx_needs/functions/common.py

View check run for this annotation

Codecov / codecov/patch

sphinx_needs/functions/common.py#L193

Added line #L193 was not covered by tests

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")

Check warning on line 340 in sphinx_needs/functions/common.py

View check run for this annotation

Codecov / codecov/patch

sphinx_needs/functions/common.py#L340

Added line #L340 was not covered by tests

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")

Check warning on line 458 in sphinx_needs/functions/common.py

View check run for this annotation

Codecov / codecov/patch

sphinx_needs/functions/common.py#L458

Added line #L458 was not covered by tests

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")

Check warning on line 546 in sphinx_needs/functions/common.py

View check run for this annotation

Codecov / codecov/patch

sphinx_needs/functions/common.py#L545-L546

Added lines #L545 - L546 were not covered by tests

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 @@ -22,6 +22,7 @@
from sphinx_needs.data import NeedsInfoType, NeedsMutable, NeedsView, SphinxNeedsData
from sphinx_needs.debug import measure_time_func
from sphinx_needs.logging import get_logger, log_warning
from sphinx_needs.roles.need_func import NeedFunc
from sphinx_needs.utils import NEEDS_FUNCTIONS, match_variants

logger = get_logger(__name__)
Expand All @@ -37,7 +38,7 @@ class DynamicFunction(Protocol):
def __call__(
self,
app: Sphinx,
need: NeedsInfoType,
need: NeedsInfoType | None,
needs: NeedsView,
*args: Any,
**kwargs: Any,
Expand Down Expand Up @@ -75,7 +76,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 @@ -111,13 +112,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 @@ -151,10 +162,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 @@ -355,7 +367,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 @@ -368,12 +383,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)
4 changes: 4 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 @@ -25,3 +27,5 @@ DYNAMIC FUNCTIONS
:tags: [[copy('id')]]

Test a `link <http://www.[[copy('id')]]>`_

This should warn since it has no associated need: :need_func:`[[copy("id")]]`
20 changes: 18 additions & 2 deletions tests/test_dynamic_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,30 @@

@pytest.mark.parametrize(
"test_app",
[{"buildername": "html", "srcdir": "doc_test/doc_dynamic_functions"}],
[
{
"buildername": "html",
"srcdir": "doc_test/doc_dynamic_functions",
"no_plantuml": True,
}
],
indirect=True,
)
def test_doc_dynamic_functions(test_app):
app = test_app
app.build()

warnings = strip_colors(
app._warning.getvalue().replace(str(app.srcdir) + os.sep, "srcdir/")
).splitlines()
# print(warnings)
assert warnings == [
"srcdir/index.rst:31: 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 @@ -44,7 +60,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 TEST_3. args:" in html
assert "Test output of need_func; need: TEST_3" in html

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

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 0d4d18e

Please sign in to comment.