Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Add c.this_doc() check for needextend filters #1393

Merged
merged 9 commits into from
Feb 11, 2025
20 changes: 13 additions & 7 deletions docs/directives/needextend.rst
Original file line number Diff line number Diff line change
Expand Up @@ -92,16 +92,22 @@ Default: false
We have a configuration (conf.py) option called :ref:`needs_needextend_strict`
that deactivates or activates the ``:strict:`` option behaviour for all ``needextend`` directives in a project.

Setting default option values
-----------------------------
Extending needs in current page
-------------------------------

.. versionadded:: 4.3.0

Additionally, to common :ref:`filter_string` variables, the ``c.this_doc()`` function is made available,
to filter for needs only in the same document as the ``needextend``.

You can use ``needextend``'s filter string to set default option values for a group of needs.

The following example would set the status of all needs in the document
``docs/directives/needextend.rst``, which do not have the status set explicitly, to ``open``.
The following example would set the status of all needs in the current document,
which do not have the status set explicitly, to ``open``.

.. code-block:: rst
.. need-example::

.. needextend:: (docname == "docs/directives/needextend") and (status is None)
.. needextend:: c.this_doc() and status is None
:status: open

See also: :ref:`needs_global_options` for setting a default option value for all needs.
Expand Down Expand Up @@ -177,5 +183,5 @@ Also filtering for these values is supported:

.. needtable::
:filter: "needextend" in title
:columns: id, title, is_modified, modifications
:columns: id, title, status, is_modified, modifications
:style: table
6 changes: 5 additions & 1 deletion sphinx_needs/directives/needextend.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,11 @@ def extend_needs_data(
else:
try:
found_needs = filter_needs_mutable(
all_needs, needs_config, need_filter, location=location
all_needs,
needs_config,
need_filter,
location=location,
origin_docname=current_needextend["docname"],
)
except Exception as e:
log_warning(
Expand Down
30 changes: 28 additions & 2 deletions sphinx_needs/filter_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@
*,
location: tuple[str, int | None] | nodes.Node | None = None,
append_warning: str = "",
origin_docname: str | None = None,
) -> list[NeedsInfoType]:
return filter_needs(
needs.values(),
Expand All @@ -291,6 +292,7 @@
current_need,
location=location,
append_warning=append_warning,
origin_docname=origin_docname,
)


Expand Down Expand Up @@ -489,6 +491,7 @@
*,
location: tuple[str, int | None] | nodes.Node | None = None,
append_warning: str = "",
origin_docname: str | None = None,
) -> list[NeedsInfoType]:
"""
Filters given needs based on a given filter string.
Expand Down Expand Up @@ -520,6 +523,7 @@
needs,
current_need,
filter_compiled=filter_compiled,
origin_docname=origin_docname,
):
found_needs.append(filter_need)
except Exception as e:
Expand Down Expand Up @@ -549,16 +553,20 @@
needs: Iterable[NeedsInfoType] | None = None,
current_need: NeedsInfoType | None = None,
filter_compiled: CodeType | None = None,
*,
origin_docname: str | None = None,
) -> bool:
"""
Checks if a single need/need_part passes a filter_string

:param need: the data for a single need
:param config: NeedsSphinxConfig object
:param filter_compiled: An already compiled filter_string to safe time
:param need: need or need_part
:param filter_string: string, which is used as input for eval()
:param needs: list of all needs
:param current_need: set the current_need in the filter context as this, otherwise the need itself
:param filter_compiled: An already compiled filter_string to save time
:param origin_docname: The origin docname that the filter was called from, if any

:return: True, if need passes the filter_string, else False
"""
filter_context: dict[str, Any] = need.copy() # type: ignore
Expand All @@ -573,6 +581,9 @@
filter_context.update(config.filter_data)

filter_context["search"] = need_search

filter_context["c"] = NeedCheckContext(need, origin_docname)

result = False
try:
# Set filter_context as globals and not only locals in eval()!
Expand All @@ -588,3 +599,18 @@
except Exception as e:
raise NeedsInvalidFilter(f"Filter {filter_string!r} not valid. Error: {e}.")
return result


class NeedCheckContext:
"""A namespace for filter checks of the current need."""

__slots__ = ("_need", "_origin_docname")

def __init__(self, need: NeedsInfoType, origin_docname: str | None) -> None:
self._need = need
self._origin_docname = origin_docname

def this_doc(self) -> bool:
if self._origin_docname is None:
raise ValueError("`this_doc` can not be used in this context")

Check warning on line 615 in sphinx_needs/filter_common.py

View check run for this annotation

Codecov / codecov/patch

sphinx_needs/filter_common.py#L615

Added line #L615 was not covered by tests
return self._need["docname"] == self._origin_docname
2 changes: 1 addition & 1 deletion tests/doc_test/doc_needextend/page_1.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ need objects
:status: open
:tags: tag_1

.. needextend:: extend_test_page
.. needextend:: "c.this_doc()"
:status: closed

4 changes: 3 additions & 1 deletion tests/test_needextend.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@

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

assert not app._warning.getvalue()

needs_data = json.loads(Path(app.outdir, "needs.json").read_text())
assert needs_data == snapshot(exclude=props("created", "project", "creator"))

Expand Down
Loading