diff --git a/docs/builders.rst b/docs/builders.rst index 52e37fc43..ee3acbf7b 100644 --- a/docs/builders.rst +++ b/docs/builders.rst @@ -163,3 +163,21 @@ or .. hint:: As an alternative, you can set the config option :ref:`needs_build_needumls` to export the needumls files during each build. + + +.. _needs_id_builder: + +needs_id +-------- +.. versionadded:: 1.4.0 + +The **needs_id** builder exports all found needs and selected filter results to a set json files of each need with the name is ``id`` of need. + +The build creates a folder called :ref:``needs_build_json_per_id_path`` and all file json of each need inside the given build-folder. + +Usage ++++++ + +.. code-block:: bash + + sphinx-build -b needs_id source_dir build_dir \ No newline at end of file diff --git a/docs/changelog.rst b/docs/changelog.rst index 8a1638835..3c7072ddc 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -7,7 +7,7 @@ License ------- .. include:: ../LICENSE - + 2.0.0 ----- Released: under development @@ -15,6 +15,7 @@ Released: under development 1.4.0 ----- Released: under development +* Improvement: Added Builder :ref:`needs_id_builder` added and config option :ref:`needs_build_json_per_id` in `conf.py`. * Improvement: Reduce document build time, by memoizing the inline parse in ``build_need`` (`#968 `_) @@ -29,7 +30,6 @@ Released: 16.08.2023 * Improvement: Configuration option :ref:`needs_debug_measurement` added, which creates a runtime report for debugging purposes. (`#917 `_) - * Bugfix: Replace hardcoded `index` with config value `root_doc`. (`#877 `_) * Bugfix: Fix unbounded memory usage in pickle environment. @@ -914,3 +914,5 @@ custom css definitions you need to update them. * Integrated interaction with the activated plantuml sphinx extension * Added role **need** to create a reference to a need by giving the id + + diff --git a/docs/conf.py b/docs/conf.py index 07abd022c..969433171 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -361,6 +361,9 @@ def custom_defined_func(): # build needs.json to make permalinks work needs_build_json = True +# build needs_json for every needs-id to make detail panel +needs_build_json_per_id = False + # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] diff --git a/docs/configuration.rst b/docs/configuration.rst index 6cb9532ed..7eecc3c63 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -2289,3 +2289,49 @@ If true, need options like status, tags or links are collapsed and shown only af Default value: True Can be overwritten for each single need by setting :ref:`need_collapse`. + +.. _needs_build_json_per_id: + +needs_build_json_per_id +~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 1.4.0 + +Builds list json files for each need. The name of each file is the ``id`` of need. +This option works like :ref:`needs_build_json`. + +Default: False + +Example: + +.. code-block:: python + + needs_build_json_per_id = False + +.. hint:: + + The created single json file per need, located in :ref:`needs_build_json_per_id_path` folder, e.g ``_build/needs_id/abc_432.json`` + +.. _needs_build_json_per_id_path: + +needs_build_json_per_id_path +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 1.4.0 + +This option sets the location of the set of ``needs.json`` for every needs-id. + +Default value: ``needs_id`` + +Example: + +.. code-block:: python + + needs_build_json_per_id_path = "needs_id" + +.. hint:: + + The created ``needs_id`` folder gets stored in the ``outdir`` of the current builder. The final location is e.g. ``_build/needs_id`` + + + diff --git a/sphinx_needs/builder.py b/sphinx_needs/builder.py index 4b1277837..1fc3255b1 100644 --- a/sphinx_needs/builder.py +++ b/sphinx_needs/builder.py @@ -160,3 +160,74 @@ def build_needumls_pumls(app: Sphinx, _exception: Exception) -> None: needs_builder.set_environment(env) needs_builder.finish() + + +class NeedsIdBuilder(Builder): + """Json builder for needs, which creates separate json-files per need""" + + name = "needs_id" + format = "needs" + file_suffix = ".txt" + links_suffix = None + + def write_doc(self, docname: str, doctree: nodes.document) -> None: + pass + + def finish(self) -> None: + env = unwrap(self.env) + data = SphinxNeedsData(env) + needs = data.get_or_create_needs().values() # We need a list of needs for later filter checks + version = getattr(env.config, "version", "unset") + needs_config = NeedsSphinxConfig(env.config) + filter_string = needs_config.builder_filter + from sphinx_needs.filter_common import filter_needs + + filtered_needs = filter_needs(self.app, needs, 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): + os.makedirs(needs_dir, exist_ok=True) + for need in filtered_needs: + needs_list = NeedsList(env.config, self.outdir, self.srcdir) + needs_list.wipe_version(version) + needs_list.add_need(version, need) + id = need["id"] + try: + file_name = f"{id}.json" + needs_list.write_json(file_name, needs_dir) + except Exception as e: + log.error(f"Needs-ID Builder {id} error: {e}") + log.info("Needs_id successfully exported") + + def get_outdated_docs(self) -> Iterable[str]: + return [] + + def prepare_writing(self, _docnames: Set[str]) -> None: + pass + + def write_doc_serialized(self, _docname: str, _doctree: nodes.document) -> None: + pass + + def cleanup(self) -> None: + pass + + def get_target_uri(self, _docname: str, _typ: Optional[str] = None) -> str: + return "" + + +def build_needs_id_json(app: Sphinx, _exception: Exception) -> None: + env = unwrap(app.env) + + if not NeedsSphinxConfig(env.config).build_json_per_id: + return + + # Do not create an additional needs_json for every needs_id, if builder is already "needs_id". + if isinstance(app.builder, NeedsIdBuilder): + return + try: + needs_id_builder = NeedsIdBuilder(app, env) + except TypeError: + needs_id_builder = NeedsIdBuilder(app) + needs_id_builder.set_environment(env) + + needs_id_builder.finish() diff --git a/sphinx_needs/config.py b/sphinx_needs/config.py index bb0c4ddc8..f7877dcb8 100644 --- a/sphinx_needs/config.py +++ b/sphinx_needs/config.py @@ -230,6 +230,9 @@ def __setattr__(self, name: str, value: Any) -> None: """Jinja context for rendering templates""" debug_measurement: bool = field(default=False, metadata={"rebuild": "html", "types": (bool,)}) + # add config for needs_id_builder + build_json_per_id: bool = field(default=False, metadata={"rebuild": "html", "types": (bool,)}) + build_json_per_id_path: str = field(default="needs_id", metadata={"rebuild": "html", "types": (str,)}) @classmethod def add_config_values(cls, app: Sphinx) -> None: diff --git a/sphinx_needs/needs.py b/sphinx_needs/needs.py index 03831baee..b4c4902dd 100644 --- a/sphinx_needs/needs.py +++ b/sphinx_needs/needs.py @@ -11,7 +11,9 @@ import sphinx_needs.debug as debug # Need to set global var in it for timeing measurements from sphinx_needs.builder import ( NeedsBuilder, + NeedsIdBuilder, NeedumlsBuilder, + build_needs_id_json, build_needs_json, build_needumls_pumls, ) @@ -139,6 +141,7 @@ def setup(app: Sphinx) -> Dict[str, Any]: app.add_builder(NeedsBuilder) app.add_builder(NeedumlsBuilder) + app.add_builder(NeedsIdBuilder) NeedsSphinxConfig.add_config_values(app) @@ -237,6 +240,8 @@ def setup(app: Sphinx) -> Dict[str, Any]: app.connect("env-updated", install_lib_static_files) app.connect("env-updated", install_permalink_file) + # + app.connect("build-finished", build_needs_id_json) # This should be called last, so that need-styles can override styles from used libraries app.connect("env-updated", install_styles_static_files) diff --git a/sphinx_needs/needsfile.py b/sphinx_needs/needsfile.py index 625a4a657..23a06d6fc 100644 --- a/sphinx_needs/needsfile.py +++ b/sphinx_needs/needsfile.py @@ -93,13 +93,17 @@ def wipe_version(self, version: str) -> None: if version in self.needs_list["versions"]: del self.needs_list["versions"][version] - def write_json(self, needs_file: str = "needs.json") -> None: + def write_json(self, needs_file: str = "needs.json", needs_path: str = "") -> None: # We need to rewrite some data, because this kind of data gets overwritten during needs.json import. self.needs_list["created"] = datetime.now().isoformat() self.needs_list["current_version"] = self.current_version self.needs_list["project"] = self.project + if needs_path: + needs_dir = needs_path + else: + needs_dir = self.outdir - with open(os.path.join(self.outdir, needs_file), "w") as f: + with open(os.path.join(needs_dir, needs_file), "w") as f: json.dump(self.needs_list, f, indent=4, sort_keys=True) def load_json(self, file: str) -> None: diff --git a/tests/test_needs_id_builder.py b/tests/test_needs_id_builder.py new file mode 100644 index 000000000..6f0f2ec8c --- /dev/null +++ b/tests/test_needs_id_builder.py @@ -0,0 +1,36 @@ +import json +from pathlib import Path + +import pytest + +from sphinx_needs.config import NeedsSphinxConfig +from sphinx_needs.data import SphinxNeedsData + + +@pytest.mark.parametrize( + "test_app", [{"buildername": "needs_id", "srcdir": "doc_test/doc_needs_builder"}], indirect=True +) +def test_doc_needs_id_builder(test_app): + import os + + from sphinx_needs.utils import unwrap + + app = test_app + app.build() + out_dir = app.outdir + env = unwrap(app.env) + data = SphinxNeedsData(env) + needs_config = NeedsSphinxConfig(env.config) + needs = data.get_or_create_needs().values() # We need a list of needs for later filter checks + needs_build_json_per_id_path = needs_config.build_json_per_id_path + needs_id_path = os.path.join(out_dir, needs_build_json_per_id_path) + assert os.path.exists(needs_id_path) + for need in needs: + need_id = need["id"] + need_file_name = f"{need_id}.json" + needs_json = Path(needs_id_path, need_file_name) + assert os.path.exists(needs_json) + with open(needs_json) as needs_file: + needs_file_content = needs_file.read() + needs_list = json.loads(needs_file_content) + assert needs_list["versions"]["1.0"]["needs"][need_id]["docname"]