Skip to content

Commit

Permalink
♻️ Make needextend argument deterministic
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisjsewell committed Jan 27, 2025
1 parent 663d8d4 commit 05652f7
Show file tree
Hide file tree
Showing 3 changed files with 61 additions and 63 deletions.
46 changes: 23 additions & 23 deletions docs/directives/needextend.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ needextend
==========
.. versionadded:: 0.7.0

``needextend`` allows to modify existing needs. It doesnt provide any output, as the modifications
``needextend`` allows to modify existing needs. It doesn't provide any output, as the modifications
get presented at the original location of the changing need, for example:

.. code-block:: rst
Expand All @@ -20,7 +20,12 @@ The following modifications are supported:
* ``+option``: add new value to an existing value of an option.
* ``-option``: delete a complete option.

The argument of ``needextend`` must be a :ref:`filter_string` which defines the needs to modify.
The argument of ``needextend`` will be taken as, by order of priority:

- a single need ID, if it is enclosed by ``<>``,
- a :ref:`filter_string` if it is enclosed by ``""``,
- a single need ID, if it is a single word (no spaces),
- a :ref:`filter_string` otherwise.

``needextend`` can modify all string-based and list-based options.
Also, you can add links or delete tags.
Expand All @@ -40,11 +45,26 @@ Also, you can add links or delete tags.
| And a tag was added.
| Finally all links got removed.
.. needextend:: id == "extend_test_001"
.. req:: needextend Example 2
:id: extend_test_002
:tags: extend_example
:status: open

Contents

.. needextend:: extend_test_001
:status: closed
:+author: and me

.. needextend:: <extend_test_001>
:+tags: new_tag

.. needextend:: id == "extend_test_002"
:status: New status

.. needextend:: ""extend_example" in tags"
:+tags: other

Options
-------

Expand Down Expand Up @@ -72,26 +92,6 @@ 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.


Single need modification
------------------------
If only one single need shall get modified, the argument of ``needextend`` can just be the need-id.

.. need-example::

.. req:: needextend Example 2
:id: extend_test_002
:status: open

.. needextend:: extend_test_002
:status: New status

.. attention::

The given argument must fully match the regular expression defined in
:ref:`needs_id_regex` and a need with this ID must exist!
Otherwise the argument is taken as normal filter string.

Setting default option values
-----------------------------
You can use ``needextend``'s filter string to set default option values for a group of needs.
Expand Down
8 changes: 5 additions & 3 deletions sphinx_needs/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -566,9 +566,11 @@ class NeedsBarType(NeedsBaseDataType):
class NeedsExtendType(NeedsBaseDataType):
"""Data to modify existing need(s)."""

filter: None | str
"""Single need ID or filter string to select multiple needs."""
modifications: dict[str, str]
filter: str
"""Filter string to select needs to extebd."""
filter_is_id: bool
"""Whether the filter is a single need ID."""
modifications: dict[str, Any]
"""Mapping of field name to new value.
If the field name starts with a ``+``, the new value is appended to the existing value.
If the field name starts with a ``-``, the existing value is cleared (new value is ignored).
Expand Down
70 changes: 33 additions & 37 deletions sphinx_needs/directives/needextend.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from __future__ import annotations

import re
from collections.abc import Sequence
from typing import Any, Callable

Expand Down Expand Up @@ -54,20 +53,37 @@ def run(self) -> Sequence[nodes.Node]:
f"Filter of needextend must be set. See {env.docname}:{self.lineno}"
)

strict = NeedsSphinxConfig(self.env.app.config).needextend_strict
needs_config = NeedsSphinxConfig(self.env.app.config)
strict = needs_config.needextend_strict
strict_option: str = self.options.get("strict", "").upper()
if strict_option == "TRUE":
strict = True
elif strict_option == "FALSE":
strict = False

modifications = self.options.copy()
modifications.pop("strict", None)

extend_filter = extend_filter.strip()
if extend_filter.startswith("<") and extend_filter.endswith(">"):
filter_is_id = True
extend_filter = extend_filter[1:-1]

Check warning on line 70 in sphinx_needs/directives/needextend.py

View check run for this annotation

Codecov / codecov/patch

sphinx_needs/directives/needextend.py#L69-L70

Added lines #L69 - L70 were not covered by tests
elif extend_filter.startswith('"') and extend_filter.endswith('"'):
filter_is_id = False
extend_filter = extend_filter[1:-1]

Check warning on line 73 in sphinx_needs/directives/needextend.py

View check run for this annotation

Codecov / codecov/patch

sphinx_needs/directives/needextend.py#L72-L73

Added lines #L72 - L73 were not covered by tests
elif len(extend_filter.split()) == 1:
filter_is_id = True
else:
filter_is_id = False

data = SphinxNeedsData(env).get_or_create_extends()
data[targetid] = {
"docname": env.docname,
"lineno": self.lineno,
"target_id": targetid,
"filter": self.arguments[0] if self.arguments else None,
"modifications": self.options,
"filter": extend_filter,
"filter_is_id": filter_is_id,
"modifications": modifications,
"strict": strict,
}

Expand All @@ -91,48 +107,28 @@ def extend_needs_data(

for current_needextend in extends.values():
need_filter = current_needextend["filter"]
if need_filter and need_filter in all_needs:
# a single known ID
found_needs = [all_needs[need_filter]]
elif need_filter is not None and re.fullmatch(
needs_config.id_regex, need_filter
):
# an unknown ID
error = f"Provided id {need_filter!r} for needextend does not exist."
if current_needextend["strict"]:
raise NeedsInvalidFilter(error)
else:
log_warning(
logger,
error,
"needextend",
location=(
current_needextend["docname"],
current_needextend["lineno"],
),
)
continue
location = (current_needextend["docname"], current_needextend["lineno"])
if current_needextend["filter_is_id"]:
try:
found_needs = [all_needs[need_filter]]
except KeyError:
error = f"Provided id {need_filter!r} for needextend does not exist."
if current_needextend["strict"]:
raise NeedsInvalidFilter(error)

Check warning on line 117 in sphinx_needs/directives/needextend.py

View check run for this annotation

Codecov / codecov/patch

sphinx_needs/directives/needextend.py#L117

Added line #L117 was not covered by tests
else:
log_warning(logger, error, "needextend", location=location)
continue
else:
# a filter string
try:
found_needs = filter_needs_mutable(
all_needs,
needs_config,
need_filter,
location=(
current_needextend["docname"],
current_needextend["lineno"],
),
all_needs, needs_config, need_filter, location=location
)
except NeedsInvalidFilter as e:
log_warning(
logger,
f"Invalid filter {need_filter!r}: {e}",
"needextend",
location=(
current_needextend["docname"],
current_needextend["lineno"],
),
location=location,
)
continue

Expand Down

0 comments on commit 05652f7

Please sign in to comment.