Skip to content
Merged
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
2 changes: 1 addition & 1 deletion docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ Schema

.. automodule:: sphinx_needs.needs_schema
:members: FieldsSchema, FieldSchema, FieldFunctionArray, LinksFunctionArray,
FieldLiteralValue, LinkSchema, LinksLiteralValue, AllowedTypes
FieldLiteralValue, LinkSchema, LinkDisplayConfig, LinksLiteralValue, AllowedTypes

.. automodule:: sphinx_needs.schema.config
:members: ExtraOptionStringSchemaType, ExtraOptionBooleanSchemaType,
Expand Down
10 changes: 5 additions & 5 deletions sphinx_needs/api/need.py
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,7 @@ def generate_need(
else v
for k, v in links_no_defaults.items()
}
_copy_links(links, needs_config)
_copy_links(links, needs_schema)

title, title_func = _convert_to_str_func("title", title_converted)
status, status_func = _convert_to_none_str_func(
Expand Down Expand Up @@ -1156,15 +1156,15 @@ def _make_hashed_id(

def _copy_links(
links: dict[str, LinksLiteralValue | LinksFunctionArray | None],
config: NeedsSphinxConfig,
schema: FieldsSchema,
) -> None:
"""Implement 'copy' logic for links."""
if "links" not in links:
return # should not happen, but be defensive
copy_links: list[str | DynamicFunctionParsed | VariantFunctionParsed] = []
for link_type in config.extra_links:
if link_type.get("copy", False) and (name := link_type["option"]) != "links":
other = links[name]
for link_field in schema.iter_link_fields():
if link_field.copy and link_field.name != "links":
other = links[link_field.name]
if isinstance(other, LinksLiteralValue | LinksFunctionArray):
copy_links.extend(other.value)
if any(
Expand Down
4 changes: 2 additions & 2 deletions sphinx_needs/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -688,10 +688,10 @@ def functions(self) -> Mapping[str, NeedFunctionsType]:
default="→\xa0", metadata={"rebuild": "html", "types": (str,)}
)
"""Prefix for need_part output in tables"""
extra_links: list[LinkOptionsType] = field(
_extra_links: list[LinkOptionsType] = field(
default_factory=list, metadata={"rebuild": "html", "types": ()}
)
"""List of additional link types between needs"""
"""List of additional link types between needs (internal config, use schema for access after config resolution)"""
report_dead_links: bool = field(
default=True, metadata={"rebuild": "html", "types": (bool,)}
)
Expand Down
4 changes: 3 additions & 1 deletion sphinx_needs/directives/list2need.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from sphinx.util.docutils import SphinxDirective

from sphinx_needs.config import NeedsSphinxConfig
from sphinx_needs.data import SphinxNeedsData

NEED_TEMPLATE = """.. {{type}}:: {{title}}
{% if need_id is not none %}:id: {{need_id}}{%endif%}
Expand Down Expand Up @@ -100,7 +101,8 @@ def run(self) -> Sequence[nodes.Node]:
down_links_raw_list = []
else:
down_links_raw_list = [x.strip() for x in down_links_raw.split(",")]
link_types = [x["option"] for x in needs_config.extra_links]
needs_schema = SphinxNeedsData(self.env).get_schema()
link_types = [link.name for link in needs_schema.iter_link_fields()]
for i, down_link_raw in enumerate(down_links_raw_list):
down_links_types[i] = down_link_raw
if down_link_raw not in link_types:
Expand Down
10 changes: 7 additions & 3 deletions sphinx_needs/directives/need.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from sphinx_needs.logging import WarningSubTypes, get_logger, log_warning
from sphinx_needs.need_constraints import process_constraints
from sphinx_needs.need_item import NeedItem, NeedItemSourceDirective
from sphinx_needs.needs_schema import FieldsSchema
from sphinx_needs.nodes import Need
from sphinx_needs.utils import (
DummyOptionSpec,
Expand Down Expand Up @@ -356,11 +357,12 @@ def post_process_needs_data(app: Sphinx) -> None:
needs_data = SphinxNeedsData(app.env)
if not needs_data.needs_is_post_processed:
needs_config = NeedsSphinxConfig(app.config)
needs_schema = needs_data.get_schema()
needs = needs_data.get_needs_mutable()
app.emit("needs-before-post-processing", needs)
extend_needs_data(needs, needs_data.get_or_create_extends(), needs_config)
resolve_functions(app, needs, needs_config)
update_back_links(needs, needs_config)
update_back_links(needs, needs_config, needs_schema)
process_constraints(needs, needs_config)
app.emit("needs-before-sealing", needs)
# run a last check to ensure all needs are of the correct type
Expand Down Expand Up @@ -430,7 +432,9 @@ def format_need_nodes(
node_need.parent.replace(node_need, rendered_node)


def update_back_links(needs: NeedsMutable, config: NeedsSphinxConfig) -> None:
def update_back_links(
needs: NeedsMutable, config: NeedsSphinxConfig, schema: FieldsSchema
) -> None:
"""Update needs with back-links, i.e. for each need A that links to need B,"""
for need in needs.values():
need.reset_backlinks()
Expand All @@ -455,7 +459,7 @@ def update_back_links(needs: NeedsMutable, config: NeedsSphinxConfig) -> None:

need["has_dead_links"] = bool(dead_links)
allow_dead_links = {
li["option"]: li.get("allow_dead_links", False) for li in config.extra_links
link.name: link.allow_dead_links for link in schema.iter_link_fields()
}
need["has_forbidden_dead_links"] = bool(
any(not allow_dead_links.get(lt, False) for lt, _ in dead_links)
Expand Down
4 changes: 3 additions & 1 deletion sphinx_needs/directives/needflow/_directive.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from sphinx_needs.data import (
GraphvizStyleType,
NeedsFlowType,
SphinxNeedsData,
)
from sphinx_needs.debug import measure_time
from sphinx_needs.filter_common import FilterBase
Expand Down Expand Up @@ -73,7 +74,8 @@ def run(self) -> Sequence[nodes.Node]:
id = self.env.new_serialno("needflow")
targetid = f"needflow-{self.env.docname}-{id}"

all_link_types = ",".join(x["option"] for x in needs_config.extra_links)
needs_schema = SphinxNeedsData(self.env).get_schema()
all_link_types = ",".join(link.name for link in needs_schema.iter_link_fields())
link_types = split_link_types(
self.options.get("link_types", all_link_types), location
)
Expand Down
38 changes: 19 additions & 19 deletions sphinx_needs/directives/needflow/_graphviz.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
)
from sphinx.util.logging import getLogger

from sphinx_needs.config import LinkOptionsType, NeedsSphinxConfig
from sphinx_needs.config import NeedsSphinxConfig
from sphinx_needs.data import SphinxNeedsData
from sphinx_needs.debug import measure_time
from sphinx_needs.directives.needflow._directive import NeedflowGraphiz
Expand All @@ -28,6 +28,7 @@
)
from sphinx_needs.logging import log_warning
from sphinx_needs.need_item import NeedItem, NeedPartItem
from sphinx_needs.needs_schema import LinkSchema
from sphinx_needs.utils import remove_node_from_tree
from sphinx_needs.variants import match_variants
from sphinx_needs.views import NeedsView
Expand All @@ -50,10 +51,11 @@ def process_needflow_graphviz(
found_nodes: list[nodes.Element],
) -> None:
needs_config = NeedsSphinxConfig(app.config)
needs_schema = SphinxNeedsData(app.env).get_schema()
env_data = SphinxNeedsData(app.env)
needs_view = env_data.get_needs_view()

link_type_names = [link["option"].upper() for link in needs_config.extra_links]
link_type_names = [name.upper() for name in needs_schema.iter_link_field_names()]
allowed_link_types_options = [link.upper() for link in needs_config.flow_link_types]

node: NeedflowGraphiz
Expand Down Expand Up @@ -93,29 +95,28 @@ def process_needflow_graphviz(
None,
)

# compute the allowed link names
allowed_link_types: list[LinkOptionsType] = []
for link_type in needs_config.extra_links:
# compute the allowed link types
allowed_link_types: list[LinkSchema] = []
for link in needs_schema.iter_link_fields():
# Skip link-type handling, if it is not part of a specified list of allowed link_types or
# if not part of the overall configuration of needs_flow_link_types
if (
attributes["link_types"]
and link_type["option"].upper() not in option_link_types
attributes["link_types"] and link.name.upper() not in option_link_types
) or (
not attributes["link_types"]
and link_type["option"].upper() not in allowed_link_types_options
and link.name.upper() not in allowed_link_types_options
):
continue
# skip creating links from child needs to their own parent need
if link_type["option"] == "parent_needs":
if link.name == "parent_needs":
continue
allowed_link_types.append(link_type)
allowed_link_types.append(link)

init_filtered_needs = (
filter_by_tree(
needs_view,
root_id,
allowed_link_types,
[lt.name for lt in allowed_link_types],
attributes["root_direction"],
attributes["root_depth"],
)
Expand Down Expand Up @@ -169,7 +170,7 @@ def process_needflow_graphviz(
content += "\n// edge definitions\n"
for need in filtered_needs:
for link_type in allowed_link_types:
for link in need[link_type["option"]]:
for link in need[link_type.name]:
content += _render_edge(
need, link, link_type, node, needs_config, rendered_nodes
)
Expand Down Expand Up @@ -440,7 +441,7 @@ def _label(
def _render_edge(
need: NeedItem | NeedPartItem,
link: str,
link_type: LinkOptionsType,
link_type: LinkSchema,
node: NeedflowGraphiz,
config: NeedsSphinxConfig,
rendered_nodes: dict[str, _RenderedNode],
Expand All @@ -455,16 +456,15 @@ def _render_edge(
params: list[tuple[str, str]] = []

if show_links:
params.append(("label", _quote(link_type["outgoing"])))
params.append(("label", _quote(link_type.display.outgoing)))

is_part = "." in link or "." in need["id_complete"]
params.extend(
# TODO also use link_type.display.color?
_style_params_from_link_type(
link_type.get("style_part", "dotted")
if is_part
else link_type.get("style", ""),
link_type.get("style_start", "-"),
link_type.get("style_end", "->"),
link_type.display.style_part if is_part else link_type.display.style,
link_type.display.style_start,
link_type.display.style_end,
)
)

Expand Down
57 changes: 24 additions & 33 deletions sphinx_needs/directives/needflow/_plantuml.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from jinja2 import Template
from sphinx.application import Sphinx

from sphinx_needs.config import LinkOptionsType, NeedsSphinxConfig
from sphinx_needs.config import NeedsSphinxConfig
from sphinx_needs.data import NeedsFlowType, SphinxNeedsData
from sphinx_needs.debug import measure_time
from sphinx_needs.diagrams_common import calculate_link, create_legend
Expand All @@ -17,6 +17,7 @@
from sphinx_needs.filter_common import filter_single_need, process_filters
from sphinx_needs.logging import get_logger, log_warning
from sphinx_needs.need_item import NeedItem, NeedPartItem
from sphinx_needs.needs_schema import LinkSchema
from sphinx_needs.utils import remove_node_from_tree
from sphinx_needs.variants import match_variants
from sphinx_needs.views import NeedsView
Expand Down Expand Up @@ -193,8 +194,9 @@ def process_needflow_plantuml(
needs_config = NeedsSphinxConfig(app.config)
env_data = SphinxNeedsData(env)
needs_view = env_data.get_needs_view()
needs_schema = env_data.get_schema()

link_type_names = [link["option"].upper() for link in needs_config.extra_links]
link_type_names = [link.name.upper() for link in needs_schema.iter_link_fields()]
allowed_link_types_options = [link.upper() for link in needs_config.flow_link_types]

node: NeedflowPlantuml
Expand All @@ -219,23 +221,23 @@ def process_needflow_plantuml(
location=node,
)

# compute the allowed link names
allowed_link_types: list[LinkOptionsType] = []
for link_type in needs_config.extra_links:
# compute the allowed link types
allowed_link_types: list[LinkSchema] = []
for link_field in needs_schema.iter_link_fields():
# Skip link-type handling, if it is not part of a specified list of allowed link_types or
# if not part of the overall configuration of needs_flow_link_types
if (
current_needflow["link_types"]
and link_type["option"].upper() not in option_link_types
and link_field.name.upper() not in option_link_types
) or (
not current_needflow["link_types"]
and link_type["option"].upper() not in allowed_link_types_options
and link_field.name.upper() not in allowed_link_types_options
):
continue
# skip creating links from child needs to their own parent need
if link_type["option"] == "parent_needs":
if link_field.name == "parent_needs":
continue
allowed_link_types.append(link_type)
allowed_link_types.append(link_field)

try:
if "sphinxcontrib.plantuml" not in app.extensions:
Expand All @@ -256,7 +258,7 @@ def process_needflow_plantuml(
filter_by_tree(
needs_view,
root_id,
allowed_link_types,
[lt.name for lt in allowed_link_types],
current_needflow["root_direction"],
current_needflow["root_depth"],
)
Expand Down Expand Up @@ -386,7 +388,7 @@ def process_needflow_plantuml(

def render_connections(
found_needs: list[NeedItem | NeedPartItem],
allowed_link_types: list[LinkOptionsType],
allowed_link_types: list[LinkSchema],
show_links: bool,
) -> str:
"""
Expand All @@ -395,7 +397,7 @@ def render_connections(
puml_connections = ""
for need_info in found_needs:
for link_type in allowed_link_types:
for link in need_info[link_type["option"]]:
for link in need_info[link_type.name]:
# Do not create an links, if the link target is not part of the search result.
if link not in [
x["id"] for x in found_needs if x["is_need"]
Expand All @@ -405,40 +407,29 @@ def render_connections(
continue

if show_links:
desc = link_type["outgoing"] + "\\n"
desc = link_type.display.outgoing + "\\n"
comment = f": {desc}"
else:
comment = ""

# If source or target of link is a need_part, a specific style is needed
if "." in link or "." in need_info["id_complete"]:
if _style_part := link_type.get("style_part"):
link_style = f"[{_style_part}]"
else:
link_style = "[dotted]"
else:
if _style := link_type.get("style"):
link_style = f"[{_style}]"
else:
link_style = ""

if _style_start := link_type.get("style_start"):
style_start = _style_start
else:
style_start = "-"

if _style_end := link_type.get("style_end"):
style_end = _style_end
link_style = f"[{link_type.display.style_part}]"
else:
style_end = "->"
link_style = (
f"[{link_type.display.style}]"
if link_type.display.style
else ""
)

# TODO also use link_type.display.color?
puml_connections += "{id} {style_start}{link_style}{style_end} {link}{comment}\n".format(
id=make_entity_name(need_info["id_complete"]),
link=make_entity_name(link),
comment=comment,
link_style=link_style,
style_start=style_start,
style_end=style_end,
style_start=link_type.display.style_start,
style_end=link_type.display.style_end,
)
return puml_connections

Expand Down
Loading
Loading