Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

♻️ Refactor needs post-processing function signatures #1040

Merged
merged 1 commit into from
Sep 29, 2023
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
3 changes: 2 additions & 1 deletion sphinx_needs/api/need.py
Original file line number Diff line number Diff line change
Expand Up @@ -746,6 +746,7 @@ def _merge_global_options(app: Sphinx, needs_info, global_options) -> None:
"""Add all global defined options to needs_info"""
if global_options is None:
return
config = NeedsSphinxConfig(app.config)
for key, value in global_options.items():
# If key already exists in needs_info, this global_option got overwritten manually in current need
if key in needs_info and needs_info[key]:
Expand All @@ -762,7 +763,7 @@ def _merge_global_options(app: Sphinx, needs_info, global_options) -> None:
for single_value in values:
if len(single_value) < 2 or len(single_value) > 3:
raise NeedsInvalidException(f"global option tuple has wrong amount of parameters: {key}")
if filter_single_need(app, needs_info, single_value[1]):
if filter_single_need(needs_info, config, single_value[1]):
# Set value, if filter has matched
needs_info[key] = single_value[0]
elif len(single_value) == 3 and (key not in needs_info.keys() or len(str(needs_info[key])) > 0):
Expand Down
6 changes: 4 additions & 2 deletions sphinx_needs/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ def finish(self) -> None:
from sphinx_needs.filter_common import filter_needs

filter_string = needs_config.builder_filter
filtered_needs: List[NeedsInfoType] = filter_needs(self.app, data.get_or_create_needs().values(), filter_string)
filtered_needs: List[NeedsInfoType] = filter_needs(
data.get_or_create_needs().values(), needs_config, filter_string
)

for need in filtered_needs:
needs_list.add_need(version, need)
Expand Down Expand Up @@ -181,7 +183,7 @@ def finish(self) -> None:
filter_string = needs_config.builder_filter
from sphinx_needs.filter_common import filter_needs

filtered_needs = filter_needs(self.app, needs, filter_string)
filtered_needs = filter_needs(needs, needs_config, filter_string)
needs_build_json_per_id_path = needs_config.build_json_per_id_path
needs_dir = os.path.join(self.outdir, needs_build_json_per_id_path)
if not os.path.exists(needs_dir):
Expand Down
2 changes: 2 additions & 0 deletions sphinx_needs/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ def __init__(self, config: _SphinxConfig) -> None:
super().__setattr__("_config", config)

def __getattribute__(self, name: str) -> Any:
if name.startswith("__"):
return super().__getattribute__(name)
return getattr(super().__getattribute__("_config"), f"needs_{name}")

def __setattr__(self, name: str, value: Any) -> None:
Expand Down
35 changes: 12 additions & 23 deletions sphinx_needs/directives/need.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from sphinx_needs.api import add_need
from sphinx_needs.api.exceptions import NeedsInvalidException
from sphinx_needs.config import NEEDS_CONFIG, NeedsSphinxConfig
from sphinx_needs.data import SphinxNeedsData
from sphinx_needs.data import NeedsInfoType, SphinxNeedsData
from sphinx_needs.debug import measure_time
from sphinx_needs.defaults import NEED_DEFAULT_OPTIONS
from sphinx_needs.directives.needextend import (
Expand Down Expand Up @@ -366,11 +366,6 @@ def process_need_nodes(app: Sphinx, doctree: nodes.document, fromdocname: str) -
"""
Event handler to add title meta data (status, tags, links, ...) information to the Need node. Also processes
constraints.

:param app:
:param doctree:
:param fromdocname:
:return:
"""
needs_config = NeedsSphinxConfig(app.config)
if not needs_config.include_needs:
Expand All @@ -381,18 +376,19 @@ def process_need_nodes(app: Sphinx, doctree: nodes.document, fromdocname: str) -

env = app.env
needs_data = SphinxNeedsData(env)
needs = needs_data.get_or_create_needs()

# If no needs were defined, we do not need to do anything
if not needs_data.get_or_create_needs():
if not needs:
return

if not needs_data.needs_is_post_processed:
resolve_dynamic_values(env)
resolve_variants_options(env)
check_links(env)
create_back_links(env)
process_constraints(app)
extend_needs_data(app)
resolve_dynamic_values(needs, app)
resolve_variants_options(needs, needs_config, app.builder.tags.tags)
check_links(needs, needs_config)
create_back_links(needs, needs_config)
process_constraints(needs, needs_config)
extend_needs_data(needs, needs_data.get_or_create_extends(), needs_config)
Comment on lines +386 to +391
Copy link
Member Author

Choose a reason for hiding this comment

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

This is the key change

needs_data.needs_is_post_processed = True

for extend_node in doctree.findall(Needextend):
Expand Down Expand Up @@ -429,16 +425,13 @@ def print_need_nodes(app: Sphinx, doctree: nodes.document, fromdocname: str, fou
build_need(layout, node_need, app, fromdocname=fromdocname)


def check_links(env: BuildEnvironment) -> None:
def check_links(needs: Dict[str, NeedsInfoType], config: NeedsSphinxConfig) -> None:
"""Checks if set links are valid or are dead (referenced need does not exist.)

For needs with dead links, an extra ``has_dead_links`` field is added and,
if the link is not allowed to be dead,
the ``has_forbidden_dead_links`` field is also added.
"""
config = NeedsSphinxConfig(env.config)
data = SphinxNeedsData(env)
needs = data.get_or_create_needs()
extra_links = config.extra_links
for need in needs.values():
for link_type in extra_links:
Expand All @@ -461,17 +454,13 @@ def check_links(env: BuildEnvironment) -> None:
break # One found dead link is enough


def create_back_links(env: BuildEnvironment) -> None:
def create_back_links(needs: Dict[str, NeedsInfoType], config: NeedsSphinxConfig) -> None:
"""Create back-links in all found needs.

These are fields for each link type, ``<link_name>_back``,
which contain a list of all IDs of needs that link to the current need.
"""
data = SphinxNeedsData(env)
needs_config = NeedsSphinxConfig(env.config)
needs = data.get_or_create_needs()

for links in needs_config.extra_links:
for links in config.extra_links:
option = links["option"]
option_back = f"{option}_back"

Expand Down
2 changes: 1 addition & 1 deletion sphinx_needs/directives/needbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ def process_needbar(app: Sphinx, doctree: nodes.document, fromdocname: str, foun
if element.isdigit():
line_number.append(float(element))
else:
result = len(filter_needs(app, need_list, element))
result = len(filter_needs(need_list, needs_config, element))
line_number.append(float(result))
local_data_number.append(line_number)

Expand Down
16 changes: 6 additions & 10 deletions sphinx_needs/directives/needextend.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,11 @@

from docutils import nodes
from docutils.parsers.rst import directives
from sphinx.application import Sphinx
from sphinx.util.docutils import SphinxDirective

from sphinx_needs.api.exceptions import NeedsInvalidFilter
from sphinx_needs.config import NeedsSphinxConfig
from sphinx_needs.data import SphinxNeedsData
from sphinx_needs.data import NeedsExtendType, NeedsInfoType, SphinxNeedsData
from sphinx_needs.filter_common import filter_needs
from sphinx_needs.logging import get_logger
from sphinx_needs.utils import add_doc
Expand Down Expand Up @@ -71,11 +70,10 @@ def run(self) -> Sequence[nodes.Node]:
return [targetnode, Needextend("")]


def extend_needs_data(app: Sphinx) -> None:
def extend_needs_data(
all_needs: Dict[str, NeedsInfoType], extends: Dict[str, NeedsExtendType], needs_config: NeedsSphinxConfig
) -> None:
"""Use data gathered from needextend directives to modify fields of existing needs."""
env = app.env
needs_config = NeedsSphinxConfig(env.config)
data = SphinxNeedsData(env)

list_values = (
["tags", "links"]
Expand All @@ -84,9 +82,7 @@ def extend_needs_data(app: Sphinx) -> None:
) # back-links (incoming)
link_names = [x["option"] for x in needs_config.extra_links]

all_needs = data.get_or_create_needs()

for current_needextend in data.get_or_create_extends().values():
for current_needextend in extends.values():
need_filter = current_needextend["filter"]
if need_filter in all_needs:
# a single known ID
Expand All @@ -102,7 +98,7 @@ def extend_needs_data(app: Sphinx) -> None:
else:
# a filter string
try:
found_needs = filter_needs(app, all_needs.values(), need_filter)
found_needs = filter_needs(all_needs.values(), needs_config, need_filter)
except NeedsInvalidFilter as e:
raise NeedsInvalidFilter(
f"Filter not valid for needextend on page {current_needextend['docname']}:\n{e}"
Expand Down
4 changes: 3 additions & 1 deletion sphinx_needs/directives/needflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,9 @@ def get_need_node_rep_for_plantuml(
# We set # later, as the user may not have given a color and the node must get highlighted
node_colors.append(need_info["type_color"].replace("#", ""))

if current_needflow["highlight"] and filter_single_need(app, need_info, current_needflow["highlight"], all_needs):
if current_needflow["highlight"] and filter_single_need(
need_info, needs_config, current_needflow["highlight"], all_needs
):
node_colors.append("line:FF0000")

# need parts style use default "rectangle"
Expand Down
4 changes: 2 additions & 2 deletions sphinx_needs/directives/needgantt.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ def process_needgantt(app: Sphinx, doctree: nodes.document, fromdocname: str, fo
complete = None

if current_needgantt["milestone_filter"]:
is_milestone = filter_single_need(app, need, current_needgantt["milestone_filter"])
is_milestone = filter_single_need(need, needs_config, current_needgantt["milestone_filter"])
else:
is_milestone = False

Expand Down Expand Up @@ -259,7 +259,7 @@ def process_needgantt(app: Sphinx, doctree: nodes.document, fromdocname: str, fo
puml_node["uml"] += "\n' Constraints definition \n\n"
for need in found_needs:
if current_needgantt["milestone_filter"]:
is_milestone = filter_single_need(app, need, current_needgantt["milestone_filter"])
is_milestone = filter_single_need(need, needs_config, current_needgantt["milestone_filter"])
else:
is_milestone = False
for con_type in ("starts_with_links", "starts_after_links", "ends_with_links"):
Expand Down
5 changes: 3 additions & 2 deletions sphinx_needs/directives/needimport.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ def run(self) -> Sequence[nodes.Node]:
if version not in needs_import_list["versions"].keys():
raise VersionNotFound(f"Version {version} not found in needs import file {correct_need_import_path}")

needs_config = NeedsSphinxConfig(self.config)
# TODO this is not exactly NeedsInfoType, because the export removes/adds some keys
needs_list: Dict[str, NeedsInfoType] = needs_import_list["versions"][version]["needs"]

Expand All @@ -138,7 +139,7 @@ def run(self) -> Sequence[nodes.Node]:
# "content" is the sphinx internal name for this kind of information
filter_context["content"] = need["description"] # type: ignore[typeddict-item]
try:
if filter_single_need(self.env.app, filter_context, filter_string):
if filter_single_need(filter_context, needs_config, filter_string):
needs_list_filtered[key] = need
except Exception as e:
logger.warning(
Expand All @@ -152,7 +153,7 @@ def run(self) -> Sequence[nodes.Node]:
needs_list = needs_list_filtered

# If we need to set an id prefix, we also need to manipulate all used ids in the imported data.
extra_links = NeedsSphinxConfig(self.config).extra_links
extra_links = needs_config.extra_links
if id_prefix:
for need in needs_list.values():
for id in needs_list:
Expand Down
5 changes: 3 additions & 2 deletions sphinx_needs/directives/needpie.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,10 @@ def run(self) -> Sequence[nodes.Node]:
def process_needpie(app: Sphinx, doctree: nodes.document, fromdocname: str, found_nodes: List[nodes.Element]) -> None:
env = app.env
needs_data = SphinxNeedsData(env)
needs_config = NeedsSphinxConfig(env.config)

# NEEDFLOW
include_needs = NeedsSphinxConfig(env.config).include_needs
include_needs = needs_config.include_needs
# for node in doctree.findall(Needpie):
for node in found_nodes:
if not include_needs:
Expand Down Expand Up @@ -146,7 +147,7 @@ def process_needpie(app: Sphinx, doctree: nodes.document, fromdocname: str, foun
if line.isdigit():
sizes.append(abs(float(line)))
else:
result = len(filter_needs(app, need_list, line))
result = len(filter_needs(need_list, needs_config, line))
sizes.append(result)
elif current_needpie["filter_func"] and not content:
try:
Expand Down
4 changes: 3 additions & 1 deletion sphinx_needs/directives/needsequence.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,9 @@ def get_message_needs(
if filter:
from sphinx_needs.filter_common import filter_single_need

if not filter_single_need(app, all_needs_dict[rec_id], filter, needs=all_needs_dict.values()):
if not filter_single_need(
all_needs_dict[rec_id], NeedsSphinxConfig(app.config), filter, needs=all_needs_dict.values()
):
continue

rec_data = {"id": rec_id, "title": all_needs_dict[rec_id]["title"], "messages": []}
Expand Down
3 changes: 2 additions & 1 deletion sphinx_needs/directives/needuml.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,8 +350,9 @@ def filter(self, filter_string):
"""
Return a list of found needs that pass the given filter string.
"""
needs_config = NeedsSphinxConfig(self.app.config)

return filter_needs(self.app, list(self.needs.values()), filter_string=filter_string)
return filter_needs(list(self.needs.values()), needs_config, filter_string=filter_string)

def imports(self, *args):
if not self.parent_need_id:
Expand Down
25 changes: 12 additions & 13 deletions sphinx_needs/filter_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ def process_filters(

:return: list of needs, which passed the filters
"""
needs_config = NeedsSphinxConfig(app.config)
found_needs: list[NeedsPartsInfoType]
sort_key = filter_data["sort_by"]
if sort_key:
Expand Down Expand Up @@ -156,13 +157,13 @@ def process_filters(
if status_filter_passed and tags_filter_passed and type_filter_passed:
found_needs_by_options.append(need_info)
# Get need by filter string
found_needs_by_string = filter_needs(app, all_needs_incl_parts, filter_data["filter"])
found_needs_by_string = filter_needs(all_needs_incl_parts, needs_config, filter_data["filter"])
# Make an intersection of both lists
found_needs = intersection_of_need_results(found_needs_by_options, found_needs_by_string)
else:
# There is no other config as the one for filter string.
# So we only need this result.
found_needs = filter_needs(app, all_needs_incl_parts, filter_data["filter"])
found_needs = filter_needs(all_needs_incl_parts, needs_config, filter_data["filter"])
else:
# Provides only a copy of needs to avoid data manipulations.
context = {
Expand Down Expand Up @@ -192,7 +193,7 @@ def process_filters(
found_needs = []

# Check if config allow unsafe filters
if NeedsSphinxConfig(app.config).allow_unsafe_filters:
if needs_config.allow_unsafe_filters:
found_needs = found_dirty_needs
else:
# Just take the ids from search result and use the related, but original need
Expand All @@ -203,8 +204,7 @@ def process_filters(

# Store basic filter configuration and result global list.
# Needed mainly for exporting the result to needs.json (if builder "needs" is used).
env = app.env
filter_list = SphinxNeedsData(env).get_or_create_filters()
filter_list = SphinxNeedsData(app.env).get_or_create_filters()
found_needs_ids = [need["id_complete"] for need in found_needs]

filter_list[filter_data["target_id"]] = {
Expand Down Expand Up @@ -258,23 +258,22 @@ def intersection_of_need_results(list_a: list[T], list_b: list[T]) -> list[T]:

@measure_time("filtering")
def filter_needs(
app: Sphinx,
needs: Iterable[V],
config: NeedsSphinxConfig,
filter_string: None | str = "",
current_need: NeedsInfoType | None = None,
) -> list[V]:
"""
Filters given needs based on a given filter string.
Returns all needs, which pass the given filter.

:param app: Sphinx application object
:param needs: list of needs, which shall be filtered
:param config: NeedsSphinxConfig object
:param filter_string: strings, which gets evaluated against each need
:param current_need: current need, which uses the filter.

:return: list of found needs
"""

if not filter_string:
return list(needs)

Expand All @@ -286,7 +285,7 @@ def filter_needs(
for filter_need in needs:
try:
if filter_single_need(
app, filter_need, filter_string, needs, current_need, filter_compiled=filter_compiled
filter_need, config, filter_string, needs, current_need, filter_compiled=filter_compiled
):
found_needs.append(filter_need)
except Exception as e:
Expand All @@ -300,8 +299,8 @@ def filter_needs(

@measure_time("filtering")
def filter_single_need(
app: Sphinx,
need: NeedsInfoType,
config: NeedsSphinxConfig,
filter_string: str = "",
needs: Iterable[NeedsInfoType] | None = None,
current_need: NeedsInfoType | None = None,
Expand All @@ -310,8 +309,8 @@ def filter_single_need(
"""
Checks if a single need/need_part passes a filter_string

:param app: Sphinx application object
:param current_need:
: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()
Expand All @@ -327,7 +326,7 @@ def filter_single_need(
filter_context["current_need"] = need

# Get needs external filter data and merge to filter_context
filter_context.update(NeedsSphinxConfig(app.config).filter_data)
filter_context.update(config.filter_data)

filter_context["search"] = re.search
result = False
Expand Down
Loading