Skip to content

Commit

Permalink
👌 Improve need-extend; allow dynamic functions in lists (#1076)
Browse files Browse the repository at this point in the history
Allow for lists containing dynamic functions, for example

```restructuredtext
.. needextend:: REQ_1
   :links: REQ_1, [[func("arg")]], REQ_2
```
  • Loading branch information
chrisjsewell authored May 21, 2024
1 parent 229d2f8 commit 934a3a0
Show file tree
Hide file tree
Showing 4 changed files with 152 additions and 52 deletions.
102 changes: 53 additions & 49 deletions sphinx_needs/directives/needextend.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,29 @@ def run(self) -> Sequence[nodes.Node]:
return [targetnode, Needextend("")]


RE_ID_FUNC = re.compile(r"\s*((?P<function>\[\[[^\]]*\]\])|(?P<id>[^;,]+))\s*([;,]|$)")
"""Regex to find IDs or functions, delimited by one of `;|,`."""


def _split_value(value: str) -> Sequence[tuple[str, bool]]:
"""Split a string into a list of values.
The string is split on `;`/`,` and whitespace is removed from the start and end of each value.
If a value starts with `[[` and ends with `]]`, it is considered a function.
:return: A list of tuples, where the first item is the value and the second is True if the value is a function.
"""
if "[[" in value:
# may contain dynamic functions
return [
(m.group("function"), True)
if m.group("function")
else (m.group("id").strip(), False)
for m in RE_ID_FUNC.finditer(value)
]
return [(i.strip(), False) for i in re.split(";|,", value) if i.strip()]


def extend_needs_data(
all_needs: dict[str, NeedsInfoType],
extends: dict[str, NeedsExtendType],
Expand Down Expand Up @@ -137,33 +160,23 @@ def extend_needs_data(
if option.startswith("+"):
option_name = option[1:]
if option_name in link_names:
if value.strip().startswith("[[") and value.strip().endswith(
"]]"
): # dynamic function
need[option_name].append(value)
else:
for ref_need in [i.strip() for i in re.split(";|,", value)]:
if ref_need not in all_needs:
logger.warning(
f"Provided link id {ref_need} for needextend does not exist. [needs]",
type="needs",
location=(
current_needextend["docname"],
current_needextend["lineno"],
),
)
continue
if ref_need not in need[option_name]:
need[option_name].append(ref_need)
for item, is_function in _split_value(value):
if (not is_function) and (item not in all_needs):
logger.warning(
f"Provided link id {item} for needextend does not exist. [needs]",
type="needs",
location=(
current_needextend["docname"],
current_needextend["lineno"],
),
)
continue
if item not in need[option_name]:
need[option_name].append(item)
elif option_name in list_values:
if value.strip().startswith("[[") and value.strip().endswith(
"]]"
): # dynamic function
need[option_name].append(value)
else:
for item in [i.strip() for i in re.split(";|,", value)]:
if item not in need[option_name]:
need[option_name].append(item)
for item, _is_function in _split_value(value):
if item not in need[option_name]:
need[option_name].append(item)
else:
if need[option_name]:
# If content is already stored, we need to add some whitespace
Expand All @@ -181,29 +194,20 @@ def extend_needs_data(
else:
if option in link_names:
need[option] = []
if value.strip().startswith("[[") and value.strip().endswith(
"]]"
): # dynamic function
need[option].append(value)
else:
for ref_need in [i.strip() for i in re.split(";|,", value)]:
if ref_need not in all_needs:
logger.warning(
f"Provided link id {ref_need} for needextend does not exist. [needs]",
type="needs",
location=(
current_needextend["docname"],
current_needextend["lineno"],
),
)
continue
need[option].append(ref_need)
for item, is_function in _split_value(value):
if (not is_function) and (item not in all_needs):
logger.warning(
f"Provided link id {item} for needextend does not exist. [needs]",
type="needs",
location=(
current_needextend["docname"],
current_needextend["lineno"],
),
)
continue
need[option].append(item)
elif option in list_values:
if value.strip().startswith("[[") and value.strip().endswith(
"]]"
): # dynamic function
need[option].append(value)
else:
need[option] = [i.strip() for i in re.split(";|,", value)]
for item, _is_function in _split_value(value):
need[option].append(item)
else:
need[option] = value
70 changes: 69 additions & 1 deletion tests/__snapshots__/test_needextend.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
'layout': '',
'links': list([
'REQ_A_1',
'REQ_D_1',
'REQ_B_1',
]),
'max_amount': '',
Expand Down Expand Up @@ -278,8 +279,75 @@
'url_postfix': '',
'user': '',
}),
'REQ_D_1': dict({
'arch': dict({
}),
'avatar': '',
'closed_at': '',
'completion': '',
'constraints': list([
]),
'constraints_passed': True,
'constraints_results': dict({
}),
'content_id': 'REQ_D_1',
'created_at': '',
'delete': None,
'description': '',
'docname': 'index',
'doctype': '.rst',
'duration': '',
'external_css': 'external_link',
'external_url': None,
'full_title': 'Requirement D 1',
'has_dead_links': False,
'has_forbidden_dead_links': False,
'id': 'REQ_D_1',
'id_prefix': '',
'is_external': False,
'is_modified': False,
'is_need': True,
'is_part': False,
'jinja_content': None,
'layout': '',
'links': list([
]),
'max_amount': '',
'max_content_lines': '',
'modifications': 0,
'params': '',
'parent_need': '',
'parent_needs': list([
]),
'parts': dict({
}),
'post_template': None,
'pre_template': None,
'prefix': '',
'query': '',
'section_name': 'needextend dynamic functions',
'sections': list([
'needextend dynamic functions',
]),
'service': '',
'signature': '',
'specific': '',
'status': None,
'style': None,
'tags': list([
]),
'target_id': 'REQ_D_1',
'template': None,
'title': 'Requirement D 1',
'type': 'req',
'type_name': 'Requirement',
'updated_at': '',
'url': '',
'url_postfix': '',
'user': '',
}),
}),
'needs_amount': 4,
'needs_amount': 5,
}),
}),
})
Expand Down
7 changes: 5 additions & 2 deletions tests/doc_test/doc_needextend_dynamic/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@ needextend dynamic functions
.. req:: Requirement C 1
:id: REQ_C_1

.. req:: Requirement D 1
:id: REQ_D_1

.. needextend:: REQ_1
:links: [[get_matching_need_ids("REQ_A_")]]
:links: [[get_matching_need_ids("REQ_A_")]];REQ_D_1

.. needextend:: REQ_1
:+links: [[get_matching_need_ids("REQ_B_")]]
:+links: REQ_D_1 , [[get_matching_need_ids("REQ_B_")]]
25 changes: 25 additions & 0 deletions tests/test_needextend.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from sphinx.util.console import strip_colors
from syrupy.filters import props

from sphinx_needs.directives.needextend import _split_value


@pytest.mark.parametrize(
"test_app",
Expand Down Expand Up @@ -64,6 +66,29 @@ def test_doc_needextend_unknown_id(test_app: Sphinx):
]


@pytest.mark.parametrize(
"value,expected",
[
("a", [("a", False)]),
("a,", [("a", False)]),
("[[a]]", [("[[a]]", True)]),
("[[a]]b", [("[[a]]b", False)]),
("[[a;]],", [("[[a;]]", True)]),
("a,b;c", [("a", False), ("b", False), ("c", False)]),
("[[a]],[[b]];[[c]]", [("[[a]]", True), ("[[b]]", True), ("[[c]]", True)]),
(" a ,, b; c ", [("a", False), ("b", False), ("c", False)]),
(
" [[a]] ,, [[b]] ; [[c]] ",
[("[[a]]", True), ("[[b]]", True), ("[[c]]", True)],
),
("a,[[b]];c", [("a", False), ("[[b]]", True), ("c", False)]),
(" a ,, [[b;]] ; c ", [("a", False), ("[[b;]]", True), ("c", False)]),
],
)
def test_split_value(value, expected):
assert _split_value(value) == expected


@pytest.mark.parametrize(
"test_app",
[{"buildername": "html", "srcdir": "doc_test/doc_needextend_dynamic"}],
Expand Down

0 comments on commit 934a3a0

Please sign in to comment.