Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 23 additions & 12 deletions docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1223,24 +1223,31 @@ By default a referenced need is described by the following string:

.. code-block:: jinja

{title} ({id})
{{ title }} ({{ id }})

By using ``needs_role_need_template`` this representation can be easily adjusted to own requirements.

Here are some ideas, how it could be used inside the **conf.py** file:

.. code-block:: python

needs_role_need_template = "[{id}]: {title}"
needs_role_need_template = "-{id}-"
needs_role_need_template = "{type}: {title} ({status})"
needs_role_need_template = "{title} ({tags})"
needs_role_need_template = "{title:*^20s} - {content:.30}"
needs_role_need_template = "[{id}] {title} ({status}) {type_name}/{type} - {tags} - {links} - {links_back} - {content}"
needs_role_need_template = "[{{ id }}]: {{ title }}"
needs_role_need_template = "-{{ id }}-"
needs_role_need_template = "{{ type }}: {{ title }} ({{ status }})"
needs_role_need_template = "{{ title }} ({{ tags }})"
needs_role_need_template = "[{{ id }}] {{ title }} ({{ status }}) {{ type_name }}/{{ type }} - {{ tags }} - {{ links }} - {{ links_back }} - {{ content }}"
needs_role_need_template = "{% if type == 'spec' %}[SPEC] {{ title }}{% else %}[{{ type|upper }}] {{ title }}{% endif %}"

``needs_role_need_template`` must be a string, which supports the following placeholders:
# Multi-line template, important it the '-' in the jinja template '-}}':
needs_role_need_template = "" \
"{% if type == 'spec' %}[SPEC] {{ title -}}" \
"{% else %}[{{ type|upper }}] {{ title }}{% endif %}"

* id
``needs_role_need_template`` must be a Jinja string, which supports the following variables:

* id or id_complete
* id_parent
* id_part
* type (short version)
* type_name (long, human readable version)
* title
Expand All @@ -1249,9 +1256,13 @@ Here are some ideas, how it could be used inside the **conf.py** file:
* links, joined by ";"
* links_back, joined by ";"
* content
* is_need
* is_part

To access the same values via an object, use ``need`` (for example ``{{ need.type }}``).

All options of Python's `.format() <https://docs.python.org/3.4/library/functions.html#format>`_ function are supported.
Please see https://pyformat.info/ for more information.
Jinja filters and control structures are supported.
Please see https://jinja.palletsprojects.com/ for syntax and features.

RST-attributes like ``**bold**`` are **not** supported.

Expand Down Expand Up @@ -1753,7 +1764,7 @@ keys:
:css_class: A class name as string, which gets set in link representations like :ref:`needtable`.
The related CSS class definition must be done by the user, e.g. by :ref:`own_css`.
(*optional*) (*default*: ``external_link``)
:allow_type_coercion:
:allow_type_coercion:
Allows to enable or disable type coercion of fields for each need, and parsing of dynamic functions.
For example if the ``tags`` need field is provided as a string like ``"tag1,tag2,[[func()]]"``, it will be parsed only if this option is set to ``True``,
otherwise will fail.
Expand Down
12 changes: 7 additions & 5 deletions docs/roles.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ The role ``:need:`` will add title, id and a link to the need.

We use it to reference an existing need, without the need to keep title and link location manually in sync.

With ``[[`` and ``]]`` you can refer to defined and set :ref:`extra fields <needs_fields>`.
With ``[[`` and ``]]`` you can refer to defined and set :ref:`extra fields <needs_fields>`.

The possible variables are listed in the configuration documentation for :ref:`needs_role_need_template`.

.. need-example::

Expand All @@ -34,10 +36,10 @@ With ``[[`` and ``]]`` you can refer to defined and set :ref:`extra fields <need
You can customize the string representation by using the
configuration parameters :ref:`needs_role_need_template` and
:ref:`needs_role_need_max_title_length`.
If we find a ``[[`` in the customized string, we handle it
according to Python's ``{`` `.format() <https://docs.python.org/3.4/library/functions.html#format>`_
function.
Please see https://pyformat.info/ for more information.
``needs_role_need_template`` is rendered using Jinja syntax.
The explicit role text variant ``:need:`[[...]] <ID>``` is also
rendered as Jinja by internally converting ``[[``/``]]`` to
``{{``/``}}``.
RST-attributes like ``**bold**`` are **not** supported.

.. warning::
Expand Down
13 changes: 7 additions & 6 deletions sphinx_needs/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ class NewFieldParams:
"""
default: None | Any = None
"""Default value for the field.

Used if the field has not been specifically set, and no predicate matches.
"""

Expand Down Expand Up @@ -257,8 +257,8 @@ class ExternalSource(TypedDict, total=False):
Values can be:

- a tuple: ``(value, filter_string)``, where the default is only applied if the filter_string is fulfilled
- a tuple: ``(value, filter_string, alternative default)``,
where the default is applied if the filter_string is fulfilled,
- a tuple: ``(value, filter_string, alternative default)``,
where the default is applied if the filter_string is fulfilled,
otherwise the alternative default is used
- a list of the tuples above
- otherwise, always set as the given value
Expand Down Expand Up @@ -291,7 +291,7 @@ class NeedLinksConfig(TypedDict, total=False):
schema: NotRequired[LinkSchemaType]
"""
A JSON schema for the link option.

If given, the schema will apply to all needs that use this link option.
The schema is applied locally on unresolved links, i.e. on the list of string ids.
For more granular control and graph traversal, use the `needs_schema_definitions` configuration.
Expand Down Expand Up @@ -338,7 +338,7 @@ class NeedFields(TypedDict):
schema: NotRequired[FieldSchemaTypes]
"""
A JSON schema definition for the field.

If given, the schema will apply to all needs that use this option.
For more granular control, use the `needs_schema_definitions` configuration.
"""
Expand Down Expand Up @@ -636,7 +636,8 @@ def get_default(cls, name: str) -> Any:
)
"""Default style for the needtable."""
role_need_template: str = field(
default="{title} ({id})", metadata={"rebuild": "html", "types": (str,)}
default="{{ title }} ({{ id }})",
metadata={"rebuild": "html", "types": (str,)},
)
"""Template for the need role output."""
role_need_max_title_length: int = field(
Expand Down
64 changes: 50 additions & 14 deletions sphinx_needs/roles/need_ref.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from sphinx.application import Sphinx
from sphinx.util.nodes import make_refnode

from sphinx_needs._jinja import compile_template, render_template_string
from sphinx_needs.config import NeedsSphinxConfig
from sphinx_needs.data import SphinxNeedsData
from sphinx_needs.errors import NoUri
Expand Down Expand Up @@ -67,6 +68,20 @@ def process_need_ref(
env = app.env
needs_config = NeedsSphinxConfig(env.config)
all_needs = SphinxNeedsData(env).get_needs_view()

role_template = None
try:
role_template = compile_template(
needs_config.role_need_template, autoescape=False
)
except Exception as exc:
log_warning(
log,
f"could not compile needs_role_need_template as Jinja template: {exc}",
"link_text",
location=None,
)

# for node_need_ref in doctree.findall(NeedRef):
for node_need_ref in found_nodes:
# Let's create a dummy node, for the case we will not be able to create a real reference
Expand All @@ -87,6 +102,7 @@ def process_need_ref(
need_id_full = node_need_ref["reftarget"]
need_id_main = need_link.id
need_id_part = need_link.part
need_id_complete = need_link.to_link_string()

if need_id_main not in all_needs:
log_warning(
Expand All @@ -112,6 +128,16 @@ def process_need_ref(
target_need
) # Transform a dict in a dict of {str, str}

if need_id_part:
dict_need["id_complete"] = need_id_complete
dict_need["id_part"] = need_id_part
dict_need["is_need"] = False
dict_need["is_part"] = True
else:
dict_need["id_part"] = ""
dict_need["is_need"] = True
dict_need["is_part"] = False

# We set the id to the complete id maintained in node_need_ref["reftarget"]
dict_need["id"] = need_id_full

Expand All @@ -133,30 +159,40 @@ def process_need_ref(

link_text = ""
if ref_name and prefix in ref_name and postfix in ref_name:
# if ref_name is set and has prefix to process, we will do so.
ref_name = ref_name.replace(prefix, "{").replace(postfix, "}")
# Keep the user-facing [[...]] syntax, but render using Jinja.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe you should be able to do this in a more "proper" way, using this API.

import minijinja

env = minijinja.Environment()
env.variable_start_string = "[["
env.variable_end_string = "]]"

Maybe you could add a way to apply this via the compile_template function

ref_name = ref_name.replace(prefix, "{{").replace(postfix, "}}")
try:
link_text = ref_name.format(**dict_need)
except KeyError as e:
link_text = render_template_string(
ref_name,
{"need": dict_need, **dict_need},
autoescape=False,
)
except Exception as exc:
log_warning(
log,
f"option placeholder {e} for need {node_need_ref['reftarget']} not found",
f"invalid inline need role template for need {node_need_ref['reftarget']}: {exc}",
"link_text",
location=node_need_ref,
)
else:
if ref_name:
# If ref_name differs from the need id, we treat the "ref_name content" as title.
dict_need["title"] = ref_name
try:
link_text = needs_config.role_need_template.format(**dict_need)
except KeyError as e:
log_warning(
log,
f"the config parameter needs_role_need_template uses unsupported placeholders: {e} ",
"link_text",
location=node_need_ref,
)
if role_template is None:
link_text = f"{dict_need['title']} ({dict_need['id']})"
else:
try:
link_text = role_template.render(
{"need": dict_need, **dict_need}
)
except Exception as exc:
log_warning(
log,
f"the config parameter needs_role_need_template uses invalid Jinja syntax or variables: {exc}",
"link_text",
location=node_need_ref,
)
link_text = f"{dict_need['title']} ({dict_need['id']})"

node_need_ref[0].children[0] = nodes.Text(link_text) # type: ignore[index]

Expand Down
2 changes: 1 addition & 1 deletion tests/doc_test/doc_role_need_max_title_length/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,6 @@
},
]

needs_role_need_template = "[{id}] {title} ({status}) {type_name}/{type} - {tags} - {links} - {links_back} - {content}"
needs_role_need_template = "[{{ id }}] {{ title }} ({{ status }}) {{ type_name }}/{{ type }} - {{ tags }} - {{ links }} - {{ links_back }} - {{ content }}"

needs_role_need_max_title_length = 10
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,6 @@
},
]

needs_role_need_template = "[{id}] {title} ({status}) {type_name}/{type} - {tags} - {links} - {links_back} - {content}"
needs_role_need_template = "[{{ id }}] {{ title }} ({{ status }}) {{ type_name }}/{{ type }} - {{ tags }} - {{ links }} - {{ links_back }} - {{ content }}"

needs_role_need_max_title_length = -1
10 changes: 9 additions & 1 deletion tests/doc_test/doc_role_need_template/conf.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
extensions = ["sphinx_needs"]

needs_build_json = True

needs_types = [
{
"directive": "story",
Expand Down Expand Up @@ -31,4 +33,10 @@
},
]

needs_role_need_template = "[{id}] {title} ({status}) {type_name}/{type} - {tags} - {links} - {links_back} - {content}"
needs_role_need_template = (
""
"{% if is_need %}[NEED] {% endif -%}"
"{% if is_part %}[NEEDPART]{% endif -%}"
"[{{id}}] [{{id_complete}}] [{{id_parent}}] [{{id_part}}] "
"[{{ type|upper }}] [{{ id }}] {{ title }} ({{ status }}) {{ type_name }}/{{ type }} - {{ tags }} - {{ links }} - {{ links_back }} - {{ content }}"
)
16 changes: 16 additions & 0 deletions tests/doc_test/doc_role_need_template/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,27 @@ ROLE NEED TEMPLATE

The Tool awesome shall have a command line interface.

* :np:`(cli) Command parser support`

.. spec:: Test spec
:id: SP_TOO_002
:status: open

Test test

.. impl:: Command line implementation
:id: IM_TOO_001
:status: implemented

Implements command line interface.

Reference: :need:`SP_TOO_001`

Reference impl: :need:`IM_TOO_001`

Reference custom inline: :need:`[[ type|upper ]] <IM_TOO_001>`

Reference need part: :need:`SP_TOO_001.cli`

Reference need part custom inline: :need:`[[ title|upper ]] <SP_TOO_001.cli>`

15 changes: 14 additions & 1 deletion tests/test_role_need_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,19 @@ def test_doc_build_html(test_app):
html = Path(app.outdir, "index.html").read_text()
assert "ROLE NEED TEMPLATE" in html
assert (
"[SP_TOO_001] Command line interface (implemented) Specification/spec - test;test2 - SP_TOO_002 - - "
"[NEED] [SP_TOO_001] [SP_TOO_001] [SP_TOO_001] [] "
"[SPEC] [SP_TOO_001] Command line interface (implemented) Specification/spec - test;test2 - SP_TOO_002 - - "
"The Tool awesome shall have a command line interface." in html
)
assert (
"[NEED] [IM_TOO_001] [IM_TOO_001] [IM_TOO_001] [] "
"[IMPL] [IM_TOO_001] Command line implementation (implemented) Implementation/impl - - - - "
"Implements command line interface." in html
)
assert '<em class="xref need">IMPL</em>' in html
assert (
"[NEEDPART][SP_TOO_001.cli] [SP_TOO_001.cli] [SP_TOO_001] [cli] "
"[SPEC] [SP_TOO_001.cli] Command parser support (implemented) Specification/spec"
in html
)
assert '<em class="xref need">COMMAND PARSER SUPPORT</em>' in html
Loading