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 5, 2024
1 parent c862e9d commit a7f6d97
Show file tree
Hide file tree
Showing 12 changed files with 59 additions and 36 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
14 changes: 13 additions & 1 deletion sphinx_needs/api/need.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
NeedsTemplateException,
)
from sphinx_needs.config import 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 @@ -800,3 +800,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 814 in sphinx_needs/api/need.py

View check run for this annotation

Codecov / codecov/patch

sphinx_needs/api/need.py#L814

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

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

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

def finish(self) -> None:
post_process_needs_data(self.app)

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

filter_string = needs_config.builder_filter
filtered_needs: list[NeedsInfoType] = filter_needs(
data.get_needs_view().values(),
needs,
needs_config,
filter_string,
append_warning="(from need_builder_filter)",
Expand Down Expand Up @@ -174,8 +172,6 @@ def write(
pass

def finish(self) -> None:
post_process_needs_data(self.app)

data = SphinxNeedsData(self.env)
needs = (
data.get_needs_view().values()
Expand Down
20 changes: 17 additions & 3 deletions sphinx_needs/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -712,6 +712,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 716 in sphinx_needs/data.py

View check run for this annotation

Codecov / codecov/patch

sphinx_needs/data.py#L716

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

def remove_need(self, need_id: str) -> None:
Expand All @@ -720,6 +722,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 726 in sphinx_needs/data.py

View check run for this annotation

Codecov / codecov/patch

sphinx_needs/data.py#L726

Added line #L726 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 @@ -730,6 +734,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 738 in sphinx_needs/data.py

View check run for this annotation

Codecov / codecov/patch

sphinx_needs/data.py#L738

Added line #L738 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 @@ -744,15 +750,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 754 in sphinx_needs/data.py

View check run for this annotation

Codecov / codecov/patch

sphinx_needs/data.py#L754

Added line #L754 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 doctree.findall(Needextend):
remove_node_from_tree(extend_node)

Expand Down
2 changes: 1 addition & 1 deletion sphinx_needs/directives/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def analyse_needs_metrics(env: BuildEnvironment) -> dict[str, Any]:
:param env: Sphinx build environment
:return: Dictionary consisting of needs metrics.
"""
needs = SphinxNeedsData(env).get_needs_view()
needs = SphinxNeedsData(env).get_needs_mutable()
metric_data: dict[str, Any] = {"needs_amount": len(needs)}
needs_types = {i["directive"]: 0 for i in NeedsSphinxConfig(env.config).types}

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 @@ -162,7 +162,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
14 changes: 7 additions & 7 deletions sphinx_needs/functions/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

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, filter_single_need
from sphinx_needs.logging import log_warning
from sphinx_needs.utils import logger
Expand All @@ -23,7 +23,7 @@
def test(
app: Sphinx,
need: NeedsInfoType,
needs: NeedsView,
needs: NeedsMutable | NeedsView,
*args: Any,
**kwargs: Any,
) -> str:
Expand All @@ -50,7 +50,7 @@ def test(
def echo(
app: Sphinx,
need: NeedsInfoType,
needs: NeedsView,
needs: NeedsMutable | NeedsView,
text: str,
*args: Any,
**kwargs: Any,
Expand All @@ -74,7 +74,7 @@ def echo(
def copy(
app: Sphinx,
need: NeedsInfoType,
needs: NeedsView,
needs: NeedsMutable | NeedsView,
option: str,
need_id: str | None = None,
lower: bool = False,
Expand Down Expand Up @@ -191,7 +191,7 @@ def copy(
def check_linked_values(
app: Sphinx,
need: NeedsInfoType,
needs: NeedsView,
needs: NeedsMutable | NeedsView,
result: Any,
search_option: str,
search_value: Any,
Expand Down Expand Up @@ -360,7 +360,7 @@ def check_linked_values(
def calc_sum(
app: Sphinx,
need: NeedsInfoType,
needs: NeedsView,
needs: NeedsMutable | NeedsView,
option: str,
filter: str | None = None,
links_only: bool = False,
Expand Down Expand Up @@ -472,7 +472,7 @@ def calc_sum(
def links_from_content(
app: Sphinx,
need: NeedsInfoType,
needs: NeedsView,
needs: NeedsMutable | NeedsView,
need_id: str | None = None,
filter: str | None = None,
) -> list[str]:
Expand Down
17 changes: 9 additions & 8 deletions sphinx_needs/functions/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def __call__(
self,
app: Sphinx,
need: NeedsInfoType,
needs: NeedsView,
needs: NeedsView | NeedsMutable,
*args: Any,
**kwargs: Any,
) -> str | int | float | list[str] | list[int] | list[float] | None: ...
Expand Down Expand Up @@ -77,6 +77,7 @@ def register_func(need_function: DynamicFunction, name: str | None = None) -> No
def execute_func(
app: Sphinx,
need: NeedsInfoType,
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 @@ -115,7 +116,7 @@ def execute_func(
func_return = func(
app,
need,
SphinxNeedsData(app.env).get_needs_view(),
needs,
*func_args,
**func_kwargs,
)
Expand Down Expand Up @@ -182,7 +183,9 @@ def find_and_replace_node_content(

func_string = func_string.replace("‘", "'") # noqa: RUF001
func_string = func_string.replace("’", "'") # noqa: RUF001
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 @@ -385,7 +388,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 @@ -402,10 +405,7 @@ def _detect_and_execute_field(
content: Any, need: NeedsInfoType, 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_pattern.search(content)
if func_match is None:
Expand All @@ -415,6 +415,7 @@ def _detect_and_execute_field(
func_return = execute_func(
app,
need,
SphinxNeedsData(app.env).get_needs_mutable(),
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 @@ -506,7 +506,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
5 changes: 4 additions & 1 deletion tests/test_needs_external_needs_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ def test_doc_build_html(test_app, sphinx_test_tempdir):
["sphinx-build", "-b", "html", "-D", rf"plantuml={plantuml}", src_dir, out_dir],
capture_output=True,
)
assert not output_second.stderr
assert output_second.stderr.decode("utf-8").splitlines() == [
"WARNING: http://my_company.com/docs/v1/index.html#TEST_01: Need 'EXT_TEST_01' has unknown outgoing link 'SPEC_1' in field 'links' [needs.external_link_outgoing]",
"WARNING: ../../_build/html/index.html#TEST_01: Need 'EXT_REL_PATH_TEST_01' has unknown outgoing link 'SPEC_1' in field 'links' [needs.external_link_outgoing]",
]

# check if incremental build used
# first build output
Expand Down

0 comments on commit a7f6d97

Please sign in to comment.