Skip to content

Commit

Permalink
♻️ Introduce need views
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisjsewell committed Sep 26, 2024
1 parent 4904141 commit 1b41e78
Show file tree
Hide file tree
Showing 19 changed files with 325 additions and 178 deletions.
10 changes: 9 additions & 1 deletion docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,12 @@ Data
----

.. automodule:: sphinx_needs.data
:members: NeedsInfoType, NeedsView, NeedsPartsView
:members: NeedsInfoType

Views
-----

.. automodule:: sphinx_needs.views
:members:
:undoc-members:
:special-members: __iter__, __getitem__, __len__
4 changes: 2 additions & 2 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@
("py:class", "docutils.nodes.Node"),
("py:class", "docutils.parsers.rst.states.RSTState"),
("py:class", "docutils.statemachine.StringList"),
("py:class", "T"),
("py:class", "sphinx_needs.debug.T"),
("py:class", "_Indexes"),
]

rst_epilog = """
Expand Down Expand Up @@ -756,7 +756,7 @@ def create_tutorial_needs(app: Sphinx, _env, _docnames):
We do this dynamically, to avoid having to maintain the JSON file manually.
"""
all_data = SphinxNeedsData(app.env).get_needs_view()
all_data = SphinxNeedsData(app.env).get_needs_mutable()
writer = NeedsList(app.config, outdir=app.confdir, confdir=app.confdir)
for i in range(1, 5):
test_id = f"T_00{i}"
Expand Down
28 changes: 13 additions & 15 deletions docs/filter.rst
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ The filter string must be a valid Python expression:

A filter string gets evaluated on needs and need_parts!
A need_part inherits all options from its parent need, if the need_part has no own content for this option.
E.g. the need_part *title* is kept, but the *status* attribute is taken from its parent need.
E.g. the need_part *content* is kept, but the *status* attribute is taken from its parent need.

.. note::

Expand Down Expand Up @@ -172,11 +172,10 @@ Additional variables for :ref:`need_part`:
.. note:: If extra options were specified using :ref:`needs_extra_options` then
those will be available for use in filter expressions as well.


Finally, the following are available:

* :ref:`re_search`, as Python function for performing searches with a regular expression
* **needs** as :class:`.NeedsPartsView` object, which contains all needs and need_parts.
* **needs** as :class:`.NeedsAndPartsListView` object, which contains all needs and need_parts.

If your expression is valid and it's True, the related need is added to the filter result list.
If it is invalid or returns False, the related need is not taken into account for the current filter.
Expand Down Expand Up @@ -265,6 +264,8 @@ with the help of Python.

The used code must define a variable ``results``, which must be a list and contains the filtered needs.

The code also has access to a variable called ``needs``, which is a :class:`.NeedsAndPartsListView` instance.

.. need-example::

.. needtable::
Expand All @@ -275,18 +276,15 @@ The used code must define a variable ``results``, which must be a list and conta
# which are linked to each other.

results = []
# Lets create a needs_dict to address needs by ids more easily.
needs_dict = {x['id']: x for x in needs}

for need in needs:
if need['type'] == 'req':
for links_id in need['links']:
if needs_dict[links_id]['type'] == 'spec':
results.append(need)
results.append(needs_dict[links_id])

The code has access to a variable called ``needs``, which contains a copy of all needs.
So manipulations on the values in ``needs`` do not have any affects.

# Lets create a map view to address needs by ids more easily
needs_view = needs.to_map_view()

for need in needs_view.filter_types(["req"]).to_list_with_parts():
for links_id in need['links']:
if needs_view[links_id]['type'] == 'spec':
results.append(need)
results.append(needs_view[links_id])

This mechanism can also be a good alternative for complex filter strings to save performance.
For example if a filter string is using list comprehensions to get access to linked needs.
Expand Down
21 changes: 6 additions & 15 deletions sphinx_needs/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@
Literal,
Mapping,
NewType,
Sequence,
TypedDict,
)

from sphinx.util.logging import getLogger

from sphinx_needs.logging import log_warning
from sphinx_needs.views import NeedsView

if TYPE_CHECKING:
from docutils.nodes import Text
Expand Down Expand Up @@ -690,19 +690,6 @@ class NeedsUmlType(NeedsBaseDataType):
"""A mutable view of the needs, before resolution
"""

NeedsView = NewType("NeedsView", Mapping[str, NeedsInfoType])
"""A read-only view of the needs, after resolution
(e.g. back links have been computed etc)
"""

NeedsPartsView = NewType("NeedsPartsView", Sequence[NeedsInfoType])
"""A read-only view of a sequence of needs and parts,
after resolution (e.g. back links have been computed etc)
The parts are created by creating a copy of the need for each item in ``parts``,
and then overwriting a subset of fields with the values from the part.
"""


class SphinxNeedsData:
"""Centralised access to sphinx-needs data, stored within the Sphinx environment."""
Expand Down Expand Up @@ -771,7 +758,11 @@ def get_needs_view(self) -> NeedsView:
after the needs have been fully collected
and resolved (e.g. back links have been computed etc)
"""
return self._env_needs # type: ignore[return-value]
try:
return self.env._needs_view
except AttributeError:
self.env._needs_view = NeedsView(_needs=self._env_needs)
return self.env._needs_view

@property
def has_export_filters(self) -> bool:
Expand Down
8 changes: 2 additions & 6 deletions sphinx_needs/directives/needbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,7 @@

from sphinx_needs.config import NeedsSphinxConfig
from sphinx_needs.data import NeedsBarType, SphinxNeedsData
from sphinx_needs.filter_common import (
FilterBase,
expand_needs_view,
filter_needs_parts,
)
from sphinx_needs.filter_common import FilterBase, filter_needs_parts
from sphinx_needs.logging import get_logger, log_warning
from sphinx_needs.utils import (
add_doc,
Expand Down Expand Up @@ -296,7 +292,7 @@ def process_needbar(
# 5. process content
local_data_number = []
# adds parts to need_list
need_list = expand_needs_view(needs_data.get_needs_view())
need_list = needs_data.get_needs_view().to_list_with_parts()

for line in local_data:
line_number = []
Expand Down
3 changes: 2 additions & 1 deletion sphinx_needs/directives/needflow/_graphviz.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from sphinx.util.logging import getLogger

from sphinx_needs.config import LinkOptionsType, NeedsSphinxConfig
from sphinx_needs.data import NeedsInfoType, NeedsView, SphinxNeedsData
from sphinx_needs.data import NeedsInfoType, SphinxNeedsData
from sphinx_needs.debug import measure_time
from sphinx_needs.diagrams_common import calculate_link
from sphinx_needs.directives.needflow._directive import NeedflowGraphiz
Expand All @@ -29,6 +29,7 @@
match_variants,
remove_node_from_tree,
)
from sphinx_needs.views import NeedsView

from ._shared import create_filter_paragraph, filter_by_tree, get_root_needs

Expand Down
8 changes: 2 additions & 6 deletions sphinx_needs/directives/needflow/_plantuml.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,7 @@
)

from sphinx_needs.config import LinkOptionsType, NeedsSphinxConfig
from sphinx_needs.data import (
NeedsFlowType,
NeedsInfoType,
NeedsView,
SphinxNeedsData,
)
from sphinx_needs.data import NeedsFlowType, NeedsInfoType, SphinxNeedsData
from sphinx_needs.debug import measure_time
from sphinx_needs.diagrams_common import calculate_link, create_legend
from sphinx_needs.directives.needflow._directive import NeedflowPlantuml
Expand All @@ -28,6 +23,7 @@
match_variants,
remove_node_from_tree,
)
from sphinx_needs.views import NeedsView

from ._shared import create_filter_paragraph, filter_by_tree, get_root_needs

Expand Down
28 changes: 13 additions & 15 deletions sphinx_needs/directives/needflow/_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,25 @@
from docutils import nodes

from sphinx_needs.config import LinkOptionsType
from sphinx_needs.data import (
NeedsFlowType,
NeedsInfoType,
NeedsView,
)
from sphinx_needs.data import NeedsFlowType, NeedsInfoType
from sphinx_needs.logging import get_logger
from sphinx_needs.views import NeedsView

logger = get_logger(__name__)


def filter_by_tree(
all_needs: NeedsView,
needs_view: NeedsView,
root_id: str,
link_types: list[LinkOptionsType],
direction: Literal["both", "incoming", "outgoing"],
depth: int | None,
) -> NeedsView:
"""Filter all needs by the given ``root_id``,
and all needs that are connected to the root need by the given ``link_types``, in the given ``direction``."""
if root_id not in all_needs:
return NeedsView({})
roots = {root_id: (0, all_needs[root_id])}
if root_id not in needs_view:
return needs_view.filter_ids([])

Check warning on line 25 in sphinx_needs/directives/needflow/_shared.py

View check run for this annotation

Codecov / codecov/patch

sphinx_needs/directives/needflow/_shared.py#L25

Added line #L25 was not covered by tests
roots = {root_id: (0, needs_view[root_id])}
link_prefixes = (
("_back",)
if direction == "incoming"
Expand All @@ -37,24 +34,25 @@ def filter_by_tree(
links_to_process = [
link["option"] + d for link in link_types for d in link_prefixes
]
need_items: dict[str, NeedsInfoType] = {}

need_ids: list[str] = []
while roots:
root_id, (root_depth, root) = roots.popitem()
if root_id in need_items:
if root_id in need_ids:
continue
if depth is not None and root_depth > depth:
continue
need_items[root_id] = root
need_ids.append(root_id)
for link_type_name in links_to_process:
roots.update(
{
i: (root_depth + 1, all_needs[i])
i: (root_depth + 1, needs_view[i])
for i in root.get(link_type_name, []) # type: ignore[attr-defined]
if i in all_needs
if i in needs_view
}
)

return NeedsView(need_items)
return needs_view.filter_ids(need_ids)


def get_root_needs(found_needs: list[NeedsInfoType]) -> list[NeedsInfoType]:
Expand Down
4 changes: 2 additions & 2 deletions sphinx_needs/directives/needpie.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from sphinx_needs.data import NeedsPieType, SphinxNeedsData
from sphinx_needs.debug import measure_time
from sphinx_needs.directives.utils import no_needs_found_paragraph
from sphinx_needs.filter_common import FilterBase, expand_needs_view, filter_needs_parts
from sphinx_needs.filter_common import FilterBase, filter_needs_parts
from sphinx_needs.logging import get_logger, log_warning
from sphinx_needs.utils import (
add_doc,
Expand Down Expand Up @@ -158,7 +158,7 @@ def process_needpie(

sizes = []
# adds parts to need_list
need_list = expand_needs_view(needs_data.get_needs_view())
need_list = needs_data.get_needs_view().to_list_with_parts()
if content and not current_needpie["filter_func"]:
for line in content:
if line.isdigit():
Expand Down
8 changes: 2 additions & 6 deletions sphinx_needs/directives/needsequence.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,7 @@
)

from sphinx_needs.config import NeedsSphinxConfig
from sphinx_needs.data import (
NeedsInfoType,
NeedsSequenceType,
NeedsView,
SphinxNeedsData,
)
from sphinx_needs.data import NeedsInfoType, NeedsSequenceType, SphinxNeedsData
from sphinx_needs.diagrams_common import (
DiagramBase,
add_config,
Expand All @@ -30,6 +25,7 @@
from sphinx_needs.filter_common import FilterBase
from sphinx_needs.logging import get_logger, log_warning
from sphinx_needs.utils import add_doc, remove_node_from_tree
from sphinx_needs.views import NeedsView

logger = get_logger(__name__)

Expand Down
Loading

0 comments on commit 1b41e78

Please sign in to comment.