From 4dbfa32c39e4afd8fe0eb6e1f0c9f09e132090a7 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Wed, 29 Jan 2025 20:00:46 +0100 Subject: [PATCH 1/7] =?UTF-8?q?=F0=9F=91=8C=20Add=20`current=5Fdocname`=20?= =?UTF-8?q?variable=20for=20`needextend`=20filters?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This allows for filtering of needs only in the same document as the `needextend` itself. --- docs/directives/needextend.rst | 12 ++++++++---- sphinx_needs/directives/needextend.py | 6 +++++- sphinx_needs/filter_common.py | 8 ++++++++ 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/docs/directives/needextend.rst b/docs/directives/needextend.rst index b6799ce1b..64d5f281b 100644 --- a/docs/directives/needextend.rst +++ b/docs/directives/needextend.rst @@ -94,14 +94,18 @@ Default: false Setting default option values ----------------------------- + 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``. +Additionally, to common :ref:`filter_string` variables, the ``current_docname`` variable is made available, +to filter for needs only in the same document as the ``needextend``. -.. code-block:: rst +The following example would set the status of all needs in the current document, +which do not have the status set explicitly, to ``open``. + +.. need-example:: - .. needextend:: (docname == "docs/directives/needextend") and (status is None) + .. needextend:: docname == current_docname and status is None :status: open See also: :ref:`needs_global_options` for setting a default option value for all needs. diff --git a/sphinx_needs/directives/needextend.py b/sphinx_needs/directives/needextend.py index e1c485415..199353b2a 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, + add_context={"current_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 48821b8da..1d4680625 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 = "", + add_context: dict[str, Any] | 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, + add_context=add_context, ) @@ -489,6 +491,7 @@ def filter_needs( *, location: tuple[str, int | None] | nodes.Node | None = None, append_warning: str = "", + add_context: dict[str, Any] | 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, + add_context=add_context, ): found_needs.append(filter_need) except Exception as e: @@ -549,6 +553,8 @@ def filter_single_need( needs: Iterable[NeedsInfoType] | None = None, current_need: NeedsInfoType | None = None, filter_compiled: CodeType | None = None, + *, + add_context: dict[str, Any] | None = None, ) -> bool: """ Checks if a single need/need_part passes a filter_string @@ -562,6 +568,8 @@ def filter_single_need( :return: True, if need passes the filter_string, else False """ filter_context: dict[str, Any] = need.copy() # type: ignore + if add_context: + filter_context.update(add_context) if needs: filter_context["needs"] = needs if current_need: From 05739262910ff464bd47d9d8a56e3ad187117169 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Tue, 11 Feb 2025 11:57:49 +0100 Subject: [PATCH 2/7] change strategy --- docs/directives/needextend.rst | 14 +++++++------- sphinx_needs/directives/needextend.py | 6 +++++- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/docs/directives/needextend.rst b/docs/directives/needextend.rst index 64d5f281b..d5b1831d1 100644 --- a/docs/directives/needextend.rst +++ b/docs/directives/needextend.rst @@ -92,20 +92,20 @@ 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 +------------------------------- -You can use ``needextend``'s filter string to set default option values for a group of needs. - -Additionally, to common :ref:`filter_string` variables, the ``current_docname`` variable is made available, +Additionally, to common :ref:`filter_string` variables, the ``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 current document, which do not have the status set explicitly, to ``open``. .. need-example:: - .. needextend:: docname == current_docname and status is None + .. needextend:: this_doc(current_need) and status is None :status: open See also: :ref:`needs_global_options` for setting a default option value for all needs. @@ -181,5 +181,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 199353b2a..de34dee0c 100644 --- a/sphinx_needs/directives/needextend.py +++ b/sphinx_needs/directives/needextend.py @@ -124,7 +124,11 @@ def extend_needs_data( needs_config, need_filter, location=location, - add_context={"current_docname": current_needextend["docname"]}, + add_context={ + "this_doc": lambda need, + current_doc=current_needextend["docname"]: need["docname"] + == current_doc + }, ) except Exception as e: log_warning( From e973c451dafcdd6e2976f0c3d4b99de17a516b1f Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Tue, 11 Feb 2025 12:39:31 +0100 Subject: [PATCH 3/7] add test --- tests/doc_test/doc_needextend/page_1.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/doc_test/doc_needextend/page_1.rst b/tests/doc_test/doc_needextend/page_1.rst index 3b0d7e8c0..9de78d890 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:: "this_doc(current_need)" :status: closed From 949562f6cc0ba5123fd4be24d6f47d841c624a63 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Tue, 11 Feb 2025 21:40:16 +0100 Subject: [PATCH 4/7] change implementation --- docs/directives/needextend.rst | 4 +-- sphinx_needs/directives/needextend.py | 6 +--- sphinx_needs/filter_common.py | 39 ++++++++++++++++++------ tests/doc_test/doc_needextend/page_1.rst | 2 +- tests/test_needextend.py | 4 ++- 5 files changed, 37 insertions(+), 18 deletions(-) diff --git a/docs/directives/needextend.rst b/docs/directives/needextend.rst index d5b1831d1..72b6dcef1 100644 --- a/docs/directives/needextend.rst +++ b/docs/directives/needextend.rst @@ -95,7 +95,7 @@ Default: false Extending needs in current page ------------------------------- -Additionally, to common :ref:`filter_string` variables, the ``this_doc`` function is made available, +Additionally, to common :ref:`filter_string` variables, the ``n.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. @@ -105,7 +105,7 @@ which do not have the status set explicitly, to ``open``. .. need-example:: - .. needextend:: this_doc(current_need) and status is None + .. needextend:: n.this_doc() and status is None :status: open See also: :ref:`needs_global_options` for setting a default option value for all needs. diff --git a/sphinx_needs/directives/needextend.py b/sphinx_needs/directives/needextend.py index de34dee0c..4ef01ffe9 100644 --- a/sphinx_needs/directives/needextend.py +++ b/sphinx_needs/directives/needextend.py @@ -124,11 +124,7 @@ def extend_needs_data( needs_config, need_filter, location=location, - add_context={ - "this_doc": lambda need, - current_doc=current_needextend["docname"]: need["docname"] - == current_doc - }, + 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 1d4680625..f03866dfd 100644 --- a/sphinx_needs/filter_common.py +++ b/sphinx_needs/filter_common.py @@ -283,7 +283,7 @@ def filter_needs_mutable( *, location: tuple[str, int | None] | nodes.Node | None = None, append_warning: str = "", - add_context: dict[str, Any] | None = None, + origin_docname: str | None = None, ) -> list[NeedsInfoType]: return filter_needs( needs.values(), @@ -292,7 +292,7 @@ def filter_needs_mutable( current_need, location=location, append_warning=append_warning, - add_context=add_context, + origin_docname=origin_docname, ) @@ -491,7 +491,7 @@ def filter_needs( *, location: tuple[str, int | None] | nodes.Node | None = None, append_warning: str = "", - add_context: dict[str, Any] | None = None, + origin_docname: str | None = None, ) -> list[NeedsInfoType]: """ Filters given needs based on a given filter string. @@ -523,7 +523,7 @@ def filter_needs( needs, current_need, filter_compiled=filter_compiled, - add_context=add_context, + origin_docname=origin_docname, ): found_needs.append(filter_need) except Exception as e: @@ -554,22 +554,22 @@ def filter_single_need( current_need: NeedsInfoType | None = None, filter_compiled: CodeType | None = None, *, - add_context: dict[str, Any] | 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 safe time + :param origin_docname: The origin docname that the filter was used called from, is any + :return: True, if need passes the filter_string, else False """ filter_context: dict[str, Any] = need.copy() # type: ignore - if add_context: - filter_context.update(add_context) if needs: filter_context["needs"] = needs if current_need: @@ -581,6 +581,9 @@ def filter_single_need( filter_context.update(config.filter_data) filter_context["search"] = need_search + + filter_context["n"] = NeedContext(need, origin_docname) + result = False try: # Set filter_context as globals and not only locals in eval()! @@ -596,3 +599,21 @@ def filter_single_need( except Exception as e: raise NeedsInvalidFilter(f"Filter {filter_string!r} not valid. Error: {e}.") return result + + +class NeedContext: + """A namespace for need filters to access 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 __getattr__(self, name: str) -> Any: + return self._need[name] # type: ignore[literal-required] + + 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 diff --git a/tests/doc_test/doc_needextend/page_1.rst b/tests/doc_test/doc_needextend/page_1.rst index 9de78d890..354f3cb71 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:: "this_doc(current_need)" +.. needextend:: "n.this_doc()" :status: closed diff --git a/tests/test_needextend.py b/tests/test_needextend.py index 2dc278692..2cc7cb8cf 100644 --- a/tests/test_needextend.py +++ b/tests/test_needextend.py @@ -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")) From dcbdd3b345959bfaf22d3e8f7ee3740fc2b5a4c1 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Tue, 11 Feb 2025 21:51:36 +0100 Subject: [PATCH 5/7] `n` to `c` --- docs/directives/needextend.rst | 4 ++-- sphinx_needs/filter_common.py | 9 +++------ tests/doc_test/doc_needextend/page_1.rst | 2 +- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/docs/directives/needextend.rst b/docs/directives/needextend.rst index 72b6dcef1..40c489aeb 100644 --- a/docs/directives/needextend.rst +++ b/docs/directives/needextend.rst @@ -95,7 +95,7 @@ Default: false Extending needs in current page ------------------------------- -Additionally, to common :ref:`filter_string` variables, the ``n.this_doc()`` function is made available, +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. @@ -105,7 +105,7 @@ which do not have the status set explicitly, to ``open``. .. need-example:: - .. needextend:: n.this_doc() 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. diff --git a/sphinx_needs/filter_common.py b/sphinx_needs/filter_common.py index f03866dfd..afbf54ae1 100644 --- a/sphinx_needs/filter_common.py +++ b/sphinx_needs/filter_common.py @@ -582,7 +582,7 @@ def filter_single_need( filter_context["search"] = need_search - filter_context["n"] = NeedContext(need, origin_docname) + filter_context["c"] = NeedCheckContext(need, origin_docname) result = False try: @@ -601,8 +601,8 @@ def filter_single_need( return result -class NeedContext: - """A namespace for need filters to access the current need.""" +class NeedCheckContext: + """A namespace for filter checks of the current need.""" __slots__ = ("_need", "_origin_docname") @@ -610,9 +610,6 @@ def __init__(self, need: NeedsInfoType, origin_docname: str | None) -> None: self._need = need self._origin_docname = origin_docname - def __getattr__(self, name: str) -> Any: - return self._need[name] # type: ignore[literal-required] - def this_doc(self) -> bool: if self._origin_docname is None: raise ValueError("`this_doc` can not be used in this context") diff --git a/tests/doc_test/doc_needextend/page_1.rst b/tests/doc_test/doc_needextend/page_1.rst index 354f3cb71..8223c98a0 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:: "n.this_doc()" +.. needextend:: "c.this_doc()" :status: closed From 40cdb7c849aedebfb3de8fa4243c777960740a9f Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Tue, 11 Feb 2025 22:04:53 +0100 Subject: [PATCH 6/7] Apply suggestions from code review Co-authored-by: Marco Heinemann --- sphinx_needs/filter_common.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sphinx_needs/filter_common.py b/sphinx_needs/filter_common.py index afbf54ae1..eb4143fba 100644 --- a/sphinx_needs/filter_common.py +++ b/sphinx_needs/filter_common.py @@ -564,8 +564,8 @@ def filter_single_need( :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 safe time - :param origin_docname: The origin docname that the filter was used called from, is any + :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 """ From 9651383aeb3f340a7d027c18a5f09b21732b61cd Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Tue, 11 Feb 2025 22:08:32 +0100 Subject: [PATCH 7/7] add versionadded to docs --- docs/directives/needextend.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/directives/needextend.rst b/docs/directives/needextend.rst index 40c489aeb..b2f56651f 100644 --- a/docs/directives/needextend.rst +++ b/docs/directives/needextend.rst @@ -95,6 +95,8 @@ Default: false 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``.