Skip to content

Commit

Permalink
✨ Add get_needs_view to public API
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisjsewell committed Sep 18, 2024
1 parent 976dcd5 commit 0975799
Show file tree
Hide file tree
Showing 12 changed files with 76 additions and 45 deletions.
2 changes: 1 addition & 1 deletion docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,4 @@ Data
----

.. automodule:: sphinx_needs.data
:members: NeedsInfoType, NeedsView
:members: NeedsInfoType, NeedsMutable, NeedsView
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
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
3 changes: 2 additions & 1 deletion sphinx_needs/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
add_need_type,
get_need_types,
)
from .need import add_external_need, add_need, del_need, make_hashed_id
from .need import add_external_need, add_need, del_need, get_needs_view, make_hashed_id

__all__ = (
"add_dynamic_function",
Expand All @@ -14,5 +14,6 @@
"add_need_type",
"del_need",
"get_need_types",
"get_needs_view",
"make_hashed_id",
)
14 changes: 13 additions & 1 deletion sphinx_needs/api/need.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
NeedsTemplateException,
)
from sphinx_needs.config import NEEDS_CONFIG, GlobalOptionsType, NeedsSphinxConfig
from sphinx_needs.data import NeedsInfoType, SphinxNeedsData
from sphinx_needs.data import NeedsInfoType, NeedsView, SphinxNeedsData
from sphinx_needs.directives.needuml import Needuml, NeedumlException
from sphinx_needs.filter_common import filter_single_need
from sphinx_needs.logging import get_logger, log_warning
Expand Down Expand Up @@ -805,3 +805,15 @@ def _merge_global_options(
# has at least the key.
if key not in needs_info.keys():
needs_info[key] = ""


def get_needs_view(app: Sphinx) -> NeedsView:
"""Return a read-only view of all resolved needs.
.. important:: this should only be called within the write phase,
after the needs have been fully collected.
If not already done, this will ensure all needs are resolved
(e.g. back links have been computed etc),
and then lock the data to prevent further modification.
"""
return SphinxNeedsData(app.env).get_needs_view()

Check warning on line 819 in sphinx_needs/api/need.py

View check run for this annotation

Codecov / codecov/patch

sphinx_needs/api/need.py#L819

Added line #L819 was not covered by tests
9 changes: 4 additions & 5 deletions sphinx_needs/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@

from sphinx_needs.config import NeedsSphinxConfig
from sphinx_needs.data import SphinxNeedsData
from sphinx_needs.directives.need import post_process_needs_data
from sphinx_needs.filter_common import filter_needs_view
from sphinx_needs.logging import get_logger, log_warning
from sphinx_needs.needsfile import NeedsList

Expand Down Expand Up @@ -58,9 +56,10 @@ def write(
return super().write(build_docnames, updated_docnames, method)

def finish(self) -> None:
post_process_needs_data(self.app)
from sphinx_needs.filter_common import filter_needs_view

data = SphinxNeedsData(self.env)
needs = data.get_needs_view()
needs_config = NeedsSphinxConfig(self.env.config)
filters = data.get_or_create_filters()
version = getattr(self.env.config, "version", "unset")
Expand All @@ -84,7 +83,7 @@ def finish(self) -> None:

filter_string = needs_config.builder_filter
filtered_needs = filter_needs_view(
data.get_needs_view(),
needs,
needs_config,
filter_string,
append_warning="(from need_builder_filter)",
Expand Down Expand Up @@ -173,7 +172,7 @@ def write(
pass

def finish(self) -> None:
post_process_needs_data(self.app)
from sphinx_needs.filter_common import filter_needs_view

data = SphinxNeedsData(self.env)
version = getattr(self.env.config, "version", "unset")
Expand Down
20 changes: 17 additions & 3 deletions sphinx_needs/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,8 @@ def add_need(self, need: NeedsInfoType) -> None:
.. important:: this should only be called within the read phase,
before the needs have been fully collected and resolved.
"""
if self.needs_is_post_processed:
raise RuntimeError("Needs have already been post-processed.")

Check warning on line 734 in sphinx_needs/data.py

View check run for this annotation

Codecov / codecov/patch

sphinx_needs/data.py#L734

Added line #L734 was not covered by tests
self._env_needs[need["id"]] = need

def remove_need(self, need_id: str) -> None:
Expand All @@ -738,6 +740,8 @@ def remove_need(self, need_id: str) -> None:
.. important:: this should only be called within the read phase,
before the needs have been fully collected and resolved.
"""
if self.needs_is_post_processed:
raise RuntimeError("Needs have already been post-processed.")

Check warning on line 744 in sphinx_needs/data.py

View check run for this annotation

Codecov / codecov/patch

sphinx_needs/data.py#L744

Added line #L744 was not covered by tests
if need_id in self._env_needs:
del self._env_needs[need_id]
self.remove_need_node(need_id)
Expand All @@ -748,6 +752,8 @@ def remove_doc(self, docname: str) -> None:
.. important:: this should only be called within the read phase,
before the needs have been fully collected and resolved.
"""
if self.needs_is_post_processed:
raise RuntimeError("Needs have already been post-processed.")

Check warning on line 756 in sphinx_needs/data.py

View check run for this annotation

Codecov / codecov/patch

sphinx_needs/data.py#L756

Added line #L756 was not covered by tests
for need_id in list(self._env_needs):
if self._env_needs[need_id]["docname"] == docname:
del self._env_needs[need_id]
Expand All @@ -762,15 +768,23 @@ def get_needs_mutable(self) -> NeedsMutable:
.. important:: this should only be called within the read phase,
before the needs have been fully collected and resolved.
"""
if self.needs_is_post_processed:
raise RuntimeError("Needs have already been post-processed.")

Check warning on line 772 in sphinx_needs/data.py

View check run for this annotation

Codecov / codecov/patch

sphinx_needs/data.py#L772

Added line #L772 was not covered by tests
return self._env_needs # type: ignore[return-value]

def get_needs_view(self) -> NeedsView:
"""Return a read-only view of all needs, after resolution.
"""Return a read-only view of all resolved needs.
.. important:: this should only be called within the write phase,
after the needs have been fully collected
and resolved (e.g. back links have been computed etc)
after the needs have been fully collected.
If not already done, this will ensure all needs are resolved
(e.g. back links have been computed etc),
and then lock the data to prevent further modification.
"""
if not self.needs_is_post_processed:
from sphinx_needs.directives.need import post_process_needs_data

post_process_needs_data(self.env.app)
return self._env_needs # type: ignore[return-value]

@property
Expand Down
8 changes: 3 additions & 5 deletions sphinx_needs/directives/need.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,10 +373,10 @@ def post_process_needs_data(app: Sphinx) -> None:
After this function has been run, one should assume that the needs data is finalised,
and so in principle should be treated as read-only.
"""
needs_config = NeedsSphinxConfig(app.config)
needs_data = SphinxNeedsData(app.env)
needs = needs_data.get_needs_mutable()
if needs and not needs_data.needs_is_post_processed:
if not needs_data.needs_is_post_processed:
needs_config = NeedsSphinxConfig(app.config)
needs = needs_data.get_needs_mutable()
extend_needs_data(needs, needs_data.get_or_create_extends(), needs_config)
resolve_dynamic_values(needs, app)
resolve_variants_options(needs, needs_config, app.builder.tags)
Expand Down Expand Up @@ -404,8 +404,6 @@ def process_need_nodes(app: Sphinx, doctree: nodes.document, fromdocname: str) -
if not needs_data.get_needs_view():
return

post_process_needs_data(app)

for extend_node in list(doctree.findall(Needextend)):
remove_node_from_tree(extend_node)

Expand Down
2 changes: 1 addition & 1 deletion sphinx_needs/external_needs.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ def load_external_needs(app: Sphinx, env: BuildEnvironment, docname: str) -> Non
# check if external needs already exist
ext_need_id = need_params["id"]

need = SphinxNeedsData(env).get_needs_view().get(ext_need_id)
need = SphinxNeedsData(env).get_needs_mutable().get(ext_need_id)

if need is not None:
# check need_params for more detail
Expand Down
27 changes: 14 additions & 13 deletions sphinx_needs/functions/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@

from sphinx_needs.api.exceptions import NeedsInvalidFilter
from sphinx_needs.config import NeedsSphinxConfig
from sphinx_needs.data import NeedsInfoType, NeedsView
from sphinx_needs.data import NeedsInfoType, NeedsMutable, NeedsView
from sphinx_needs.filter_common import (
filter_needs_view,
filter_needs,
filter_single_need,
)
from sphinx_needs.logging import log_warning
Expand All @@ -26,7 +26,7 @@
def test(
app: Sphinx,
need: NeedsInfoType | None,
needs: NeedsView,
needs: NeedsMutable | NeedsView,
*args: Any,
**kwargs: Any,
) -> str:
Expand Down Expand Up @@ -54,7 +54,7 @@ def test(
def echo(
app: Sphinx,
need: NeedsInfoType | None,
needs: NeedsView,
needs: NeedsMutable | NeedsView,
text: str,
*args: Any,
**kwargs: Any,
Expand All @@ -78,7 +78,7 @@ def echo(
def copy(
app: Sphinx,
need: NeedsInfoType | None,
needs: NeedsView,
needs: NeedsMutable | NeedsView,
option: str,
need_id: str | None = None,
lower: bool = False,
Expand Down Expand Up @@ -170,14 +170,15 @@ def copy(
need = needs[need_id]

if filter:
result = filter_needs_view(
needs,
location = (

Check warning on line 173 in sphinx_needs/functions/common.py

View check run for this annotation

Codecov / codecov/patch

sphinx_needs/functions/common.py#L173

Added line #L173 was not covered by tests
(need["docname"], need["lineno"]) if need and need["docname"] else None
)
result = filter_needs(

Check warning on line 176 in sphinx_needs/functions/common.py

View check run for this annotation

Codecov / codecov/patch

sphinx_needs/functions/common.py#L176

Added line #L176 was not covered by tests
needs.values(),
NeedsSphinxConfig(app.config),
filter,
need,
location=(need["docname"], need["lineno"])
if need and need["docname"]
else None,
location=location,
)
if result:
need = result[0]
Expand All @@ -201,7 +202,7 @@ def copy(
def check_linked_values(
app: Sphinx,
need: NeedsInfoType | None,
needs: NeedsView,
needs: NeedsMutable | NeedsView,
result: Any,
search_option: str,
search_value: Any,
Expand Down Expand Up @@ -373,7 +374,7 @@ def check_linked_values(
def calc_sum(
app: Sphinx,
need: NeedsInfoType | None,
needs: NeedsView,
needs: NeedsMutable | NeedsView,
option: str,
filter: str | None = None,
links_only: bool = False,
Expand Down Expand Up @@ -488,7 +489,7 @@ def calc_sum(
def links_from_content(
app: Sphinx,
need: NeedsInfoType | None,
needs: NeedsView,
needs: NeedsMutable | NeedsView,
need_id: str | None = None,
filter: str | None = None,
) -> list[str]:
Expand Down
23 changes: 12 additions & 11 deletions sphinx_needs/functions/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def __call__(
self,
app: Sphinx,
need: NeedsInfoType | None,
needs: NeedsView,
needs: NeedsView | NeedsMutable,
*args: Any,
**kwargs: Any,
) -> str | int | float | list[str] | list[int] | list[float] | None: ...
Expand Down Expand Up @@ -78,6 +78,7 @@ def register_func(need_function: DynamicFunction, name: str | None = None) -> No
def execute_func(
app: Sphinx,
need: NeedsInfoType | None,
needs: NeedsView | NeedsMutable,
func_string: str,
location: str | tuple[str | None, int | None] | nodes.Node | None,
) -> str | int | float | list[str] | list[int] | list[float] | None:
Expand Down Expand Up @@ -118,7 +119,7 @@ def execute_func(
func_return = func(
app,
need,
SphinxNeedsData(app.env).get_needs_view(),
needs,
*func_args,
**func_kwargs,
)
Expand Down Expand Up @@ -201,7 +202,9 @@ def find_and_replace_node_content(
msg = f"The [[{func_string}]] syntax in need content is deprecated. Replace with :ndf:`{func_string}` instead."
log_warning(logger, msg, "deprecation", location=node)

func_return = execute_func(env.app, need, func_string, node)
func_return = execute_func(
env.app, need, SphinxNeedsData(env).get_needs_view(), func_string, node
)

if isinstance(func_return, list):
func_return = ", ".join(str(el) for el in func_return)
Expand Down Expand Up @@ -266,7 +269,7 @@ def resolve_dynamic_values(needs: NeedsMutable, app: Sphinx) -> None:
while func_call:
try:
func_call, func_return = _detect_and_execute_field(
need[need_option], need, app
need[need_option], need, needs, app
)
except FunctionParsingException:
raise SphinxError(
Expand Down Expand Up @@ -298,7 +301,7 @@ def resolve_dynamic_values(needs: NeedsMutable, app: Sphinx) -> None:
for element in need[need_option]:
try:
func_call, func_return = _detect_and_execute_field(
element, need, app
element, need, needs, app
)
except FunctionParsingException:
raise SphinxError(
Expand Down Expand Up @@ -401,7 +404,7 @@ def check_and_get_content(

func_call = func_match.group(1) # Extract function call
func_return = execute_func(
env.app, need, func_call, location
env.app, need, SphinxNeedsData(env).get_needs_view(), func_call, location
) # Execute function call and get return value

if isinstance(func_return, list):
Expand All @@ -415,13 +418,10 @@ def check_and_get_content(


def _detect_and_execute_field(
content: Any, need: NeedsInfoType, app: Sphinx
content: Any, need: NeedsInfoType, needs: NeedsMutable, app: Sphinx
) -> tuple[str | None, str | int | float | list[str] | list[int] | list[float] | None]:
"""Detects if given need field value is a function call and executes it."""
try:
content = str(content)
except UnicodeEncodeError:
content = content.encode("utf-8")
content = str(content)

func_match = FUNC_RE.search(content)
if func_match is None:
Expand All @@ -431,6 +431,7 @@ def _detect_and_execute_field(
func_return = execute_func(
app,
need,
needs,
func_call,
(need["docname"], need["lineno"]) if need["docname"] else None,
) # Execute function call and get return value
Expand Down
1 change: 0 additions & 1 deletion sphinx_needs/needs.py
Original file line number Diff line number Diff line change
Expand Up @@ -503,7 +503,6 @@ def prepare_env(app: Sphinx, env: BuildEnvironment, _docname: str) -> None:
"""
needs_config = NeedsSphinxConfig(app.config)
data = SphinxNeedsData(env)
data.get_needs_view()
data.get_or_create_filters()
data.get_or_create_docs()
services = data.get_or_create_services()
Expand Down
10 changes: 8 additions & 2 deletions sphinx_needs/roles/need_func.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from sphinx.environment import BuildEnvironment
from sphinx.util.docutils import SphinxRole

from sphinx_needs.data import NeedsInfoType
from sphinx_needs.data import NeedsInfoType, SphinxNeedsData
from sphinx_needs.logging import get_logger, log_warning
from sphinx_needs.utils import add_doc

Expand Down Expand Up @@ -60,7 +60,13 @@ def get_text(self, env: BuildEnvironment, need: NeedsInfoType | None) -> nodes.T
from sphinx_needs.functions.functions import check_and_get_content, execute_func

if not self.with_brackets:
func_return = execute_func(env.app, need, self.astext(), self)
func_return = execute_func(
env.app,
need,
SphinxNeedsData(env).get_needs_view(),
self.astext(),
self,
)
if isinstance(func_return, list):
func_return = ", ".join(str(el) for el in func_return)

Expand Down

0 comments on commit 0975799

Please sign in to comment.