Repository: useblocks/sphinx-needs
Pull request #1393 (2025-01-29 19:01:17)

✨ Add `c.this_doc()` check for `needextend` filters
This allows for filtering of needs only in the same document as the `needextend` itself, e.g.

```restructuredtext
.. needextend:: c.this_doc() and status is None
   :status: open
```

Putting this under the `c` (for check) namespace allows for the possibility to add future special checks, without polluting the global namespace.

cc @arwedus 

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/useblocks/sphinx-needs/pull/1393?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=useblocks) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 88.21%. Comparing base [(`4e10030`)](https://app.codecov.io/gh/useblocks/sphinx-needs/commit/4e10030581c03c59764e517ab750d3ddbb2695d8?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=useblocks) to head [(`4dbfa32`)](https://app.codecov.io/gh/useblocks/sphinx-needs/commit/4dbfa32c39e4afd8fe0eb6e1f0c9f09e132090a7?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=useblocks).
> Report is 106 commits behind head on master.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #1393      +/-   ##
==========================================
+ Coverage   86.87%   88.21%   +1.33%     
==========================================
  Files          56       60       +4     
  Lines        6532     7147     +615     
==========================================
+ Hits         5675     6305     +630     
+ Misses        857      842      -15     
```

| [Flag](https://app.codecov.io/gh/useblocks/sphinx-needs/pull/1393/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=useblocks) | Coverage Δ | |
|---|---|---|
| [pytests](https://app.codecov.io/gh/useblocks/sphinx-needs/pull/1393/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=useblocks) | `88.21% <100.00%> (+1.33%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=useblocks#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/useblocks/sphinx-needs/pull/1393?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=useblocks).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=useblocks).

ubmarco: Test case would be nice to have.
chrisjsewell: > Test case would be nice to have.

done

Patch:
diff --git a/docs/directives/needextend.rst b/docs/directives/needextend.rst
index b6799ce1..b2f56651 100644
--- a/docs/directives/needextend.rst
+++ b/docs/directives/needextend.rst
@@ -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.
@@ -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
diff --git a/sphinx_needs/directives/needextend.py b/sphinx_needs/directives/needextend.py
index e1c48541..4ef01ffe 100644
--- a/sphinx_needs/directives/needextend.py
+++ b/sphinx_needs/directives/needextend.py
@@ -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(
diff --git a/sphinx_needs/filter_common.py b/sphinx_needs/filter_common.py
index 48821b8d..eb4143fb 100644
--- a/sphinx_needs/filter_common.py
+++ b/sphinx_needs/filter_common.py
@@ -283,6 +283,7 @@ def filter_needs_mutable(
     *,
     location: tuple[str, int | None] | nodes.Node | None = None,
     append_warning: str = "",
+    origin_docname: str | None = None,
 ) -> list[NeedsInfoType]:
     return filter_needs(
         needs.values(),
@@ -291,6 +292,7 @@ def filter_needs_mutable(
         current_need,
         location=location,
         append_warning=append_warning,
+        origin_docname=origin_docname,
     )
 
 
@@ -489,6 +491,7 @@ def filter_needs(
     *,
     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.
@@ -520,6 +523,7 @@ def filter_needs(
                 needs,
                 current_need,
                 filter_compiled=filter_compiled,
+                origin_docname=origin_docname,
             ):
                 found_needs.append(filter_need)
         except Exception as e:
@@ -549,16 +553,20 @@ def filter_single_need(
     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
@@ -573,6 +581,9 @@ def filter_single_need(
     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()!
@@ -588,3 +599,18 @@ def filter_single_need(
     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")
+        return self._need["docname"] == self._origin_docname


Test patch:
diff --git a/tests/doc_test/doc_needextend/page_1.rst b/tests/doc_test/doc_needextend/page_1.rst
index 3b0d7e8c..8223c98a 100644
--- a/tests/doc_test/doc_needextend/page_1.rst
+++ b/tests/doc_test/doc_needextend/page_1.rst
@@ -7,6 +7,6 @@ need objects
    :status: open
    :tags: tag_1
 
-.. needextend:: extend_test_page
+.. needextend:: "c.this_doc()"
    :status: closed
 
diff --git a/tests/test_needextend.py b/tests/test_needextend.py
index 2dc27869..2cc7cb8c 100644
--- a/tests/test_needextend.py
+++ b/tests/test_needextend.py
@@ -10,13 +10,15 @@ from syrupy.filters import props
 
 @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"))