Repository: useblocks/sphinx-needs
Pull request #1234 (2024-08-22 15:50:52)

🧪 Add test to ensure need field jsonschemas are correct
To be overly cautious and guard against future changes

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/useblocks/sphinx-needs/pull/1234?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=useblocks) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 86.89%. Comparing base [(`4e10030`)](https://app.codecov.io/gh/useblocks/sphinx-needs/commit/4e10030581c03c59764e517ab750d3ddbb2695d8?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=useblocks) to head [(`2e42574`)](https://app.codecov.io/gh/useblocks/sphinx-needs/commit/2e425746cf2dbe5457c442c49ccb32497e176517?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=useblocks).
> Report is 5 commits behind head on master.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #1234      +/-   ##
==========================================
+ Coverage   86.87%   86.89%   +0.01%     
==========================================
  Files          56       56              
  Lines        6532     6564      +32     
==========================================
+ Hits         5675     5704      +29     
- Misses        857      860       +3     
```

| [Flag](https://app.codecov.io/gh/useblocks/sphinx-needs/pull/1234/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=useblocks) | Coverage Δ | |
|---|---|---|
| [pytests](https://app.codecov.io/gh/useblocks/sphinx-needs/pull/1234/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=useblocks) | `86.89% <ø> (+0.01%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=useblocks#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/useblocks/sphinx-needs/pull/1234?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=useblocks).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=useblocks).


Patch:
diff --git a/sphinx_needs/data.py b/sphinx_needs/data.py
index 40329532..05bc0bd4 100644
--- a/sphinx_needs/data.py
+++ b/sphinx_needs/data.py
@@ -277,7 +277,7 @@ NeedsCoreFields: Final[Mapping[str, CoreFieldParameters]] = {
     },
     "section_name": {
         "description": "Simply the first section.",
-        "schema": {"type": ["string", "null"], "default": ""},
+        "schema": {"type": "string", "default": ""},
     },
     "signature": {
         "description": "Derived from a docutils desc_name node.",
@@ -736,17 +736,6 @@ class SphinxNeedsData:
             self.env.app.needs_services = ServiceManager(self.env.app)
         return self.env.app.needs_services
 
-    def get_or_create_bars(self) -> dict[str, NeedsBarType]:
-        """Get all bar charts, mapped by ID.
-
-        This is lazily created and cached in the environment.
-        """
-        try:
-            return self.env.need_all_needbar
-        except AttributeError:
-            self.env.need_all_needbar = {}
-        return self.env.need_all_needbar
-
     def get_or_create_extends(self) -> dict[str, NeedsExtendType]:
         """Get all need modifications, mapped by ID.
 
@@ -758,96 +747,6 @@ class SphinxNeedsData:
             self.env.need_all_needextend = {}
         return self.env.need_all_needextend
 
-    def get_or_create_extracts(self) -> dict[str, NeedsExtractType]:
-        """Get all need extractions, mapped by ID.
-
-        This is lazily created and cached in the environment.
-        """
-        try:
-            return self.env.need_all_needextracts
-        except AttributeError:
-            self.env.need_all_needextracts = {}
-        return self.env.need_all_needextracts
-
-    def _get_or_create_filters(self) -> dict[str, _NeedsFilterType]:
-        """Get all need filters, mapped by ID.
-
-        .. deprecated:: 0.2.0
-
-        This is lazily created and cached in the environment.
-        """
-        try:
-            return self.env.need_all_needfilters
-        except AttributeError:
-            self.env.need_all_needfilters = {}
-        return self.env.need_all_needfilters
-
-    def get_or_create_flows(self) -> dict[str, NeedsFlowType]:
-        """Get all need flow charts, mapped by ID.
-
-        This is lazily created and cached in the environment.
-        """
-        try:
-            return self.env.need_all_needflows
-        except AttributeError:
-            self.env.need_all_needflows = {}
-        return self.env.need_all_needflows
-
-    def get_or_create_gantts(self) -> dict[str, NeedsGanttType]:
-        """Get all need gantt charts, mapped by ID.
-
-        This is lazily created and cached in the environment.
-        """
-        try:
-            return self.env.need_all_needgantts
-        except AttributeError:
-            self.env.need_all_needgantts = {}
-        return self.env.need_all_needgantts
-
-    def get_or_create_lists(self) -> dict[str, NeedsListType]:
-        """Get all need gantt charts, mapped by ID.
-
-        This is lazily created and cached in the environment.
-        """
-        try:
-            return self.env.need_all_needlists
-        except AttributeError:
-            self.env.need_all_needlists = {}
-        return self.env.need_all_needlists
-
-    def get_or_create_pies(self) -> dict[str, NeedsPieType]:
-        """Get all need gantt charts, mapped by ID.
-
-        This is lazily created and cached in the environment.
-        """
-        try:
-            return self.env.need_all_needpie
-        except AttributeError:
-            self.env.need_all_needpie = {}
-        return self.env.need_all_needpie
-
-    def get_or_create_sequences(self) -> dict[str, NeedsSequenceType]:
-        """Get all need sequence diagrams, mapped by ID.
-
-        This is lazily created and cached in the environment.
-        """
-        try:
-            return self.env.need_all_needsequences
-        except AttributeError:
-            self.env.need_all_needsequences = {}
-        return self.env.need_all_needsequences
-
-    def get_or_create_tables(self) -> dict[str, NeedsTableType]:
-        """Get all need tables, mapped by ID.
-
-        This is lazily created and cached in the environment.
-        """
-        try:
-            return self.env.need_all_needtables
-        except AttributeError:
-            self.env.need_all_needtables = {}
-        return self.env.need_all_needtables
-
     def get_or_create_umls(self) -> dict[str, NeedsUmlType]:
         """Get all need uml diagrams, mapped by ID.
 
@@ -924,14 +823,5 @@ def merge_data(
                 )
 
     _merge("needs_all_docs", is_complex_dict=True)
-    _merge("need_all_needbar")
     _merge("need_all_needextend")
-    _merge("need_all_needextracts")
-    _merge("need_all_needfilters")
-    _merge("need_all_needflows")
-    _merge("need_all_needgantts")
-    _merge("need_all_needlists")
-    _merge("need_all_needpie")
-    _merge("need_all_needsequences")
-    _merge("need_all_needtables")
     _merge("needs_all_needumls")
diff --git a/sphinx_needs/directives/needbar.py b/sphinx_needs/directives/needbar.py
index f41c286f..bc255ff1 100644
--- a/sphinx_needs/directives/needbar.py
+++ b/sphinx_needs/directives/needbar.py
@@ -9,7 +9,7 @@ from docutils.parsers.rst import directives
 from sphinx.application import Sphinx
 
 from sphinx_needs.config import NeedsSphinxConfig
-from sphinx_needs.data import SphinxNeedsData
+from sphinx_needs.data import NeedsBarType, SphinxNeedsData
 from sphinx_needs.filter_common import FilterBase, filter_needs, prepare_need_list
 from sphinx_needs.logging import get_logger
 from sphinx_needs.utils import (
@@ -127,9 +127,7 @@ class NeedbarDirective(FilterBase):
         transpose = "transpose" in self.options
         horizontal = "horizontal" in self.options
 
-        # 2. Stores infos for needbar
-        data = SphinxNeedsData(self.env).get_or_create_bars()
-        data[targetid] = {
+        data_attributes: NeedsBarType = {
             "docname": env.docname,
             "lineno": self.lineno,
             "target_id": targetid,
@@ -155,11 +153,11 @@ class NeedbarDirective(FilterBase):
             "text_color": text_color,
         }
 
-        add_doc(env, env.docname)
-
-        bar_node = Needbar("")
+        bar_node = Needbar("", **data_attributes)
         self.set_source_info(bar_node)
 
+        add_doc(env, env.docname)
+
         return [targetnode, bar_node]
 
 
@@ -202,8 +200,7 @@ def process_needbar(
             remove_node_from_tree(node)
             continue
 
-        id = node.attributes["ids"][0]
-        current_needbar = needs_data.get_or_create_bars()[id]
+        current_needbar: NeedsBarType = node.attributes
 
         if matplotlib is None:
             message = "Matplotlib missing for needbar plot"
@@ -473,7 +470,7 @@ def process_needbar(
         # 9. final storage
 
         # We need to calculate an unique bar-image file name
-        hash_value = hashlib.sha256(id.encode()).hexdigest()[:5]
+        hash_value = hashlib.sha256(node.attributes["ids"][0].encode()).hexdigest()[:5]
         image_node = save_matplotlib_figure(
             app, figure, f"need_bar_{hash_value}", fromdocname
         )
diff --git a/sphinx_needs/directives/needextract.py b/sphinx_needs/directives/needextract.py
index 9ceba1a0..7cbaefae 100644
--- a/sphinx_needs/directives/needextract.py
+++ b/sphinx_needs/directives/needextract.py
@@ -10,7 +10,7 @@ from sphinx.application import Sphinx
 
 from sphinx_needs.api.exceptions import NeedsInvalidFilter
 from sphinx_needs.config import NeedsSphinxConfig
-from sphinx_needs.data import SphinxNeedsData
+from sphinx_needs.data import NeedsExtractType, SphinxNeedsData
 from sphinx_needs.directives.utils import (
     no_needs_found_paragraph,
     used_filter_paragraph,
@@ -50,9 +50,7 @@ class NeedextractDirective(FilterBase):
 
         filter_arg = self.arguments[0] if self.arguments else None
 
-        # Add the need and all needed information
-        data = SphinxNeedsData(env).get_or_create_extracts()
-        data[targetid] = {
+        attributes: NeedsExtractType = {
             "docname": env.docname,
             "lineno": self.lineno,
             "target_id": targetid,
@@ -62,10 +60,12 @@ class NeedextractDirective(FilterBase):
             "filter_arg": filter_arg,
             **self.collect_filter_attributes(),
         }
+        node = Needextract("", **attributes)
+        self.set_source_info(node)
 
         add_doc(env, env.docname, "needextract")
 
-        return [targetnode, Needextract("")]
+        return [targetnode, node]
 
 
 def process_needextract(
@@ -85,8 +85,7 @@ def process_needextract(
             remove_node_from_tree(node)
             continue
 
-        id = node.attributes["ids"][0]
-        current_needextract = SphinxNeedsData(env).get_or_create_extracts()[id]
+        current_needextract: NeedsExtractType = node.attributes
         all_needs = SphinxNeedsData(env).get_or_create_needs()
         content: list[nodes.Element] = []
 
diff --git a/sphinx_needs/directives/needfilter.py b/sphinx_needs/directives/needfilter.py
index 158743a7..5fb20bb8 100644
--- a/sphinx_needs/directives/needfilter.py
+++ b/sphinx_needs/directives/needfilter.py
@@ -12,7 +12,7 @@ from sphinx.application import Sphinx
 from sphinx.errors import NoUri
 
 from sphinx_needs.config import NeedsSphinxConfig
-from sphinx_needs.data import SphinxNeedsData
+from sphinx_needs.data import SphinxNeedsData, _NeedsFilterType
 from sphinx_needs.diagrams_common import create_legend
 from sphinx_needs.directives.utils import no_needs_found_paragraph
 from sphinx_needs.filter_common import FilterBase, process_filters
@@ -55,9 +55,7 @@ class NeedfilterDirective(FilterBase):
         )
         targetnode = nodes.target("", "", ids=[targetid])
 
-        # Add the need and all needed information
-        data = SphinxNeedsData(env)._get_or_create_filters()
-        data[targetid] = {
+        attributes: _NeedsFilterType = {
             "docname": env.docname,
             "lineno": self.lineno,
             "target_id": targetid,
@@ -68,10 +66,12 @@ class NeedfilterDirective(FilterBase):
             "layout": self.options.get("layout", "list"),
             **self.collect_filter_attributes(),
         }
+        node = Needfilter("", **attributes)
+        self.set_source_info(node)
 
         add_doc(env, env.docname)
 
-        return [targetnode, Needfilter("")]
+        return [targetnode, node]
 
 
 def process_needfilters(
@@ -94,8 +94,7 @@ def process_needfilters(
             remove_node_from_tree(node)
             continue
 
-        id = node.attributes["ids"][0]
-        current_needfilter = SphinxNeedsData(env)._get_or_create_filters()[id]
+        current_needfilter: _NeedsFilterType = node.attributes
 
         content: nodes.Element | list[nodes.Element]
         if current_needfilter["layout"] == "list":
diff --git a/sphinx_needs/directives/needflow.py b/sphinx_needs/directives/needflow.py
index 7194a05c..79299533 100644
--- a/sphinx_needs/directives/needflow.py
+++ b/sphinx_needs/directives/needflow.py
@@ -92,9 +92,7 @@ class NeedflowDirective(FilterBase):
                 if config_name and config_name in needs_config.flow_configs:
                     configs.append(needs_config.flow_configs[config_name])
 
-        # Add the need and all needed information
-        data = SphinxNeedsData(env).get_or_create_flows()
-        data[targetid] = {
+        attributes: NeedsFlowType = {
             "docname": env.docname,
             "lineno": self.lineno,
             "target_id": targetid,
@@ -116,10 +114,12 @@ class NeedflowDirective(FilterBase):
             "caption": self.arguments[0] if self.arguments else None,
             **self.collect_filter_attributes(),
         }
+        node = Needflow("", **attributes)
+        self.set_source_info(node)
 
         add_doc(env, env.docname)
 
-        return [targetnode, Needflow("")]
+        return [targetnode, node]
 
 
 def make_entity_name(name: str) -> str:
@@ -372,8 +372,7 @@ def process_needflow(
             remove_node_from_tree(node)
             continue
 
-        id = node.attributes["ids"][0]
-        current_needflow = env_data.get_or_create_flows()[id]
+        current_needflow: NeedsFlowType = node.attributes
 
         option_link_types = [link.upper() for link in current_needflow["link_types"]]
         for lt in option_link_types:
diff --git a/sphinx_needs/directives/needgantt.py b/sphinx_needs/directives/needgantt.py
index 0f11bb26..b682a8a7 100644
--- a/sphinx_needs/directives/needgantt.py
+++ b/sphinx_needs/directives/needgantt.py
@@ -13,7 +13,7 @@ from sphinxcontrib.plantuml import (
 )
 
 from sphinx_needs.config import NeedsSphinxConfig
-from sphinx_needs.data import SphinxNeedsData
+from sphinx_needs.data import NeedsGanttType, SphinxNeedsData
 from sphinx_needs.diagrams_common import (
     DiagramBase,
     add_config,
@@ -104,8 +104,7 @@ class NeedganttDirective(FilterBase, DiagramBase):
             "completion_option", needs_config.completion_option
         )
 
-        # Add the needgantt and all needed information
-        SphinxNeedsData(env).get_or_create_gantts()[targetid] = {
+        attributes: NeedsGanttType = {
             "docname": env.docname,
             "lineno": self.lineno,
             "target_id": targetid,
@@ -122,11 +121,11 @@ class NeedganttDirective(FilterBase, DiagramBase):
             **self.collect_diagram_attributes(),
         }
 
-        add_doc(env, env.docname)
-
-        gantt_node = Needgantt("")
+        gantt_node = Needgantt("", **attributes)
         self.set_source_info(gantt_node)
 
+        add_doc(env, env.docname)
+
         return [targetnode, gantt_node]
 
     def get_link_type_option(self, name: str, default: str = "") -> list[str]:
@@ -170,8 +169,7 @@ def process_needgantt(
             remove_node_from_tree(node)
             continue
 
-        id = node.attributes["ids"][0]
-        current_needgantt = SphinxNeedsData(env).get_or_create_gantts()[id]
+        current_needgantt: NeedsGanttType = node.attributes
         all_needs_dict = SphinxNeedsData(env).get_or_create_needs()
 
         content = []
diff --git a/sphinx_needs/directives/needlist.py b/sphinx_needs/directives/needlist.py
index 461c17f5..336728a9 100644
--- a/sphinx_needs/directives/needlist.py
+++ b/sphinx_needs/directives/needlist.py
@@ -7,7 +7,7 @@ from docutils.parsers.rst import directives
 from sphinx.application import Sphinx
 
 from sphinx_needs.config import NeedsSphinxConfig
-from sphinx_needs.data import SphinxNeedsData
+from sphinx_needs.data import NeedsListType, SphinxNeedsData
 from sphinx_needs.directives.utils import (
     no_needs_found_paragraph,
     used_filter_paragraph,
@@ -46,8 +46,7 @@ class NeedlistDirective(FilterBase):
         )
         targetnode = nodes.target("", "", ids=[targetid])
 
-        # Add the need and all needed information
-        SphinxNeedsData(env).get_or_create_lists()[targetid] = {
+        attributes: NeedsListType = {
             "docname": env.docname,
             "lineno": self.lineno,
             "target_id": targetid,
@@ -56,10 +55,12 @@ class NeedlistDirective(FilterBase):
             "show_filters": "show_filters" in self.options,
             **self.collect_filter_attributes(),
         }
+        list_node = Needlist("", **attributes)
+        self.set_source_info(list_node)
 
         add_doc(env, env.docname)
 
-        return [targetnode, Needlist("")]
+        return [targetnode, list_node]
 
 
 def process_needlist(
@@ -82,8 +83,7 @@ def process_needlist(
             remove_node_from_tree(node)
             continue
 
-        id = node.attributes["ids"][0]
-        current_needfilter = SphinxNeedsData(env).get_or_create_lists()[id]
+        current_needfilter: NeedsListType = node.attributes
         content: list[nodes.Node] = []
         all_needs = list(SphinxNeedsData(env).get_or_create_needs().values())
         found_needs = process_filters(app, all_needs, current_needfilter)
diff --git a/sphinx_needs/directives/needpie.py b/sphinx_needs/directives/needpie.py
index 69cf1c99..44f3b803 100644
--- a/sphinx_needs/directives/needpie.py
+++ b/sphinx_needs/directives/needpie.py
@@ -8,7 +8,7 @@ from docutils.parsers.rst import directives
 from sphinx.application import Sphinx
 
 from sphinx_needs.config import NeedsSphinxConfig
-from sphinx_needs.data import SphinxNeedsData
+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, filter_needs, prepare_need_list
@@ -83,8 +83,7 @@ class NeedpieDirective(FilterBase):
 
         shadow = "shadow" in self.options
 
-        # Stores infos for needpie
-        SphinxNeedsData(env).get_or_create_pies()[targetid] = {
+        attributes: NeedsPieType = {
             "docname": env.docname,
             "lineno": self.lineno,
             "target_id": targetid,
@@ -100,11 +99,11 @@ class NeedpieDirective(FilterBase):
             "filter_func": self.collect_filter_attributes()["filter_func"],
             "filter_warning": self.collect_filter_attributes()["filter_warning"],
         }
-        add_doc(env, env.docname)
-
-        pie_node = Needpie("")
+        pie_node = Needpie("", **attributes)
         self.set_source_info(pie_node)
 
+        add_doc(env, env.docname)
+
         return [targetnode, pie_node]
 
 
@@ -137,8 +136,7 @@ def process_needpie(
             remove_node_from_tree(node)
             continue
 
-        id = node.attributes["ids"][0]
-        current_needpie = needs_data.get_or_create_pies()[id]
+        current_needpie: NeedsPieType = node.attributes
 
         if matplotlib is None:
             message = "Matplotlib missing for needpie plot"
@@ -296,7 +294,7 @@ def process_needpie(
         # Final storage
 
         # We need to calculate an unique pie-image file name
-        hash_value = hashlib.sha256(id.encode()).hexdigest()[:5]
+        hash_value = hashlib.sha256(node.attributes["ids"][0].encode()).hexdigest()[:5]
         image_node = save_matplotlib_figure(
             app, fig, f"need_pie_{hash_value}", fromdocname
         )
diff --git a/sphinx_needs/directives/needsequence.py b/sphinx_needs/directives/needsequence.py
index bba1269d..88ccafe7 100644
--- a/sphinx_needs/directives/needsequence.py
+++ b/sphinx_needs/directives/needsequence.py
@@ -12,7 +12,7 @@ from sphinxcontrib.plantuml import (
 )
 
 from sphinx_needs.config import NeedsSphinxConfig
-from sphinx_needs.data import NeedsInfoType, SphinxNeedsData
+from sphinx_needs.data import NeedsInfoType, NeedsSequenceType, SphinxNeedsData
 from sphinx_needs.diagrams_common import (
     DiagramBase,
     add_config,
@@ -61,8 +61,7 @@ class NeedsequenceDirective(FilterBase, DiagramBase, Exception):
                 f"See file {env.docname}:{self.lineno}"
             )
 
-        # Add the needsequence and all needed information
-        SphinxNeedsData(env).get_or_create_sequences()[targetid] = {
+        attributes: NeedsSequenceType = {
             "docname": env.docname,
             "lineno": self.lineno,
             "target_id": targetid,
@@ -70,10 +69,12 @@ class NeedsequenceDirective(FilterBase, DiagramBase, Exception):
             **self.collect_filter_attributes(),
             **self.collect_diagram_attributes(),
         }
+        node = Needsequence("", **attributes)
+        self.set_source_info(node)
 
         add_doc(env, env.docname)
 
-        return [targetnode] + [Needsequence("")]
+        return [targetnode, node]
 
 
 def process_needsequence(
@@ -99,8 +100,7 @@ def process_needsequence(
             remove_node_from_tree(node)
             continue
 
-        id = node.attributes["ids"][0]
-        current_needsequence = needs_data.get_or_create_sequences()[id]
+        current_needsequence: NeedsSequenceType = node.attributes
 
         option_link_types = [
             link.upper() for link in current_needsequence["link_types"]
diff --git a/sphinx_needs/directives/needtable.py b/sphinx_needs/directives/needtable.py
index 629e9670..b77fcbfa 100644
--- a/sphinx_needs/directives/needtable.py
+++ b/sphinx_needs/directives/needtable.py
@@ -9,7 +9,7 @@ from sphinx.application import Sphinx
 
 from sphinx_needs.api.exceptions import NeedsInvalidException
 from sphinx_needs.config import NeedsSphinxConfig
-from sphinx_needs.data import NeedsInfoType, SphinxNeedsData
+from sphinx_needs.data import NeedsInfoType, NeedsTableType, SphinxNeedsData
 from sphinx_needs.debug import measure_time
 from sphinx_needs.directives.utils import (
     get_option_list,
@@ -92,8 +92,7 @@ class NeedtableDirective(FilterBase):
         if self.arguments:
             title = self.arguments[0]
 
-        # Add the need and all needed information
-        SphinxNeedsData(env).get_or_create_tables()[targetid] = {
+        attributes: NeedsTableType = {
             "docname": env.docname,
             "lineno": self.lineno,
             "target_id": targetid,
@@ -111,10 +110,12 @@ class NeedtableDirective(FilterBase):
             "show_parts": self.options.get("show_parts", False) is None,
             **self.collect_filter_attributes(),
         }
+        node = Needtable("", **attributes)
+        self.set_source_info(node)
 
         add_doc(env, env.docname)
 
-        return [targetnode] + [Needtable("")]
+        return [targetnode, node]
 
 
 @measure_time("needtable")
@@ -154,8 +155,7 @@ def process_needtables(
             remove_node_from_tree(node)
             continue
 
-        id = node.attributes["ids"][0]
-        current_needtable = needs_data.get_or_create_tables()[id]
+        current_needtable: NeedsTableType = node.attributes
 
         if current_needtable["style"] == "" or current_needtable[
             "style"
@@ -179,7 +179,9 @@ def process_needtables(
         if style != "TABLE":
             classes.extend(needs_config.table_classes)
 
-        table_node = nodes.table(classes=classes, ids=[id + "-table_node"])
+        table_node = nodes.table(
+            classes=classes, ids=[node.attributes["ids"][0] + "-table_node"]
+        )
         tgroup = nodes.tgroup(cols=len(current_needtable["columns"]))
 
         # Define Table column width
diff --git a/sphinx_needs/directives/needuml.py b/sphinx_needs/directives/needuml.py
index ba9c35d9..20b6797f 100644
--- a/sphinx_needs/directives/needuml.py
+++ b/sphinx_needs/directives/needuml.py
@@ -129,7 +129,10 @@ class NeedumlDirective(SphinxDirective):
 
         add_doc(env, env.docname)
 
-        return [targetnode] + [Needuml(targetid)]
+        node = Needuml(targetid)
+        self.set_source_info(node)
+
+        return [targetnode, node]
 
 
 class NeedarchDirective(NeedumlDirective):


Test patch:
diff --git a/tests/__snapshots__/test_basic_doc.ambr b/tests/__snapshots__/test_basic_doc.ambr
index 88e3fb6e..ed1d8091 100644
--- a/tests/__snapshots__/test_basic_doc.ambr
+++ b/tests/__snapshots__/test_basic_doc.ambr
@@ -446,10 +446,7 @@
               'default': '',
               'description': 'Simply the first section.',
               'field_type': 'core',
-              'type': list([
-                'string',
-                'null',
-              ]),
+              'type': 'string',
             }),
             'sections': dict({
               'default': list([
diff --git a/tests/__snapshots__/test_basic_doc/test_build_html_parallel[test_app0].doctree.xml b/tests/__snapshots__/test_basic_doc/test_build_html_parallel[test_app0].doctree.xml
index 1ea1a6d6..369ab7c5 100644
--- a/tests/__snapshots__/test_basic_doc/test_build_html_parallel[test_app0].doctree.xml
+++ b/tests/__snapshots__/test_basic_doc/test_build_html_parallel[test_app0].doctree.xml
@@ -9,4 +9,4 @@
         <target anonymous="" ids="US_63252" refid="US_63252">
         <Need classes="need need-story" ids="US_63252" refid="US_63252">
         <target refid="needfilter-index-0">
-        <Needfilter ids="needfilter-index-0">
+        <Needfilter docname="index" export_id="" filter="True" filter_code="[]" filter_func="True" filter_warning="True" ids="needfilter-index-0" layout="list" lineno="14" show_filters="False" show_legend="False" show_status="False" show_tags="False" sort_by="True" status="" tags="test my_test" target_id="needfilter-index-0" types="">
diff --git a/tests/__snapshots__/test_export_id.ambr b/tests/__snapshots__/test_export_id.ambr
index d1940e46..4ad4c7d7 100644
--- a/tests/__snapshots__/test_export_id.ambr
+++ b/tests/__snapshots__/test_export_id.ambr
@@ -1042,10 +1042,7 @@
               'default': '',
               'description': 'Simply the first section.',
               'field_type': 'core',
-              'type': list([
-                'string',
-                'null',
-              ]),
+              'type': 'string',
             }),
             'sections': dict({
               'default': list([
diff --git a/tests/__snapshots__/test_external.ambr b/tests/__snapshots__/test_external.ambr
index db8ed5ea..621f255b 100644
--- a/tests/__snapshots__/test_external.ambr
+++ b/tests/__snapshots__/test_external.ambr
@@ -346,10 +346,7 @@
               'default': '',
               'description': 'Simply the first section.',
               'field_type': 'core',
-              'type': list([
-                'string',
-                'null',
-              ]),
+              'type': 'string',
             }),
             'sections': dict({
               'default': list([
@@ -1123,10 +1120,7 @@
               'default': '',
               'description': 'Simply the first section.',
               'field_type': 'core',
-              'type': list([
-                'string',
-                'null',
-              ]),
+              'type': 'string',
             }),
             'sections': dict({
               'default': list([
diff --git a/tests/__snapshots__/test_import.ambr b/tests/__snapshots__/test_import.ambr
index 4b55c941..32006133 100644
--- a/tests/__snapshots__/test_import.ambr
+++ b/tests/__snapshots__/test_import.ambr
@@ -4845,10 +4845,7 @@
               'default': '',
               'description': 'Simply the first section.',
               'field_type': 'core',
-              'type': list([
-                'string',
-                'null',
-              ]),
+              'type': 'string',
             }),
             'sections': dict({
               'default': list([
diff --git a/tests/__snapshots__/test_need_constraints.ambr b/tests/__snapshots__/test_need_constraints.ambr
index a0524690..0d7cbd30 100644
--- a/tests/__snapshots__/test_need_constraints.ambr
+++ b/tests/__snapshots__/test_need_constraints.ambr
@@ -882,10 +882,7 @@
               'default': '',
               'description': 'Simply the first section.',
               'field_type': 'core',
-              'type': list([
-                'string',
-                'null',
-              ]),
+              'type': 'string',
             }),
             'sections': dict({
               'default': list([
diff --git a/tests/__snapshots__/test_needextend.ambr b/tests/__snapshots__/test_needextend.ambr
index d17d5c61..ff62ef5f 100644
--- a/tests/__snapshots__/test_needextend.ambr
+++ b/tests/__snapshots__/test_needextend.ambr
@@ -650,10 +650,7 @@
               'default': '',
               'description': 'Simply the first section.',
               'field_type': 'core',
-              'type': list([
-                'string',
-                'null',
-              ]),
+              'type': 'string',
             }),
             'sections': dict({
               'default': list([
@@ -1507,10 +1504,7 @@
               'default': '',
               'description': 'Simply the first section.',
               'field_type': 'core',
-              'type': list([
-                'string',
-                'null',
-              ]),
+              'type': 'string',
             }),
             'sections': dict({
               'default': list([
diff --git a/tests/__snapshots__/test_needs_builder.ambr b/tests/__snapshots__/test_needs_builder.ambr
index 52f9a73f..a474441b 100644
--- a/tests/__snapshots__/test_needs_builder.ambr
+++ b/tests/__snapshots__/test_needs_builder.ambr
@@ -514,10 +514,7 @@
               'default': '',
               'description': 'Simply the first section.',
               'field_type': 'core',
-              'type': list([
-                'string',
-                'null',
-              ]),
+              'type': 'string',
             }),
             'sections': dict({
               'default': list([
@@ -1174,10 +1171,7 @@
               'default': '',
               'description': 'Simply the first section.',
               'field_type': 'core',
-              'type': list([
-                'string',
-                'null',
-              ]),
+              'type': 'string',
             }),
             'sections': dict({
               'default': list([
@@ -1979,10 +1973,7 @@
               'default': '',
               'description': 'Simply the first section.',
               'field_type': 'core',
-              'type': list([
-                'string',
-                'null',
-              ]),
+              'type': 'string',
             }),
             'sections': dict({
               'default': list([
diff --git a/tests/__snapshots__/test_service_github.ambr b/tests/__snapshots__/test_service_github.ambr
index 7e4b113b..e169c7ff 100644
--- a/tests/__snapshots__/test_service_github.ambr
+++ b/tests/__snapshots__/test_service_github.ambr
@@ -628,10 +628,7 @@
               'default': '',
               'description': 'Simply the first section.',
               'field_type': 'core',
-              'type': list([
-                'string',
-                'null',
-              ]),
+              'type': 'string',
             }),
             'sections': dict({
               'default': list([
diff --git a/tests/test_data.py b/tests/test_data.py
index e7f38c0d..9cc6f62b 100644
--- a/tests/test_data.py
+++ b/tests/test_data.py
@@ -1,5 +1,6 @@
 import ast
 import inspect
+from typing import ForwardRef
 
 from sphinx_needs.data import NeedsCoreFields, NeedsInfoType
 
@@ -10,8 +11,42 @@ def test_consistent():
     but I'm not sure this is possible (to encode both the static and dynamic data required).
     So at least here, we check that they are consistent with each other.
     """
+    # check fields are consistent
     assert set(NeedsCoreFields).issubset(set(NeedsInfoType.__annotations__))
 
+    # check field types are consistent with schema
+    for field, data in NeedsCoreFields.items():
+        if not (schema := data.get("schema")):
+            continue
+        type_ = NeedsInfoType.__annotations__[field]
+        assert isinstance(type_, ForwardRef)
+        type_str = type_.__forward_arg__
+        if type_str.startswith("Required"):
+            type_str = type_str[9:-1]
+        if type_str == "str" or type_str == "str | Text":
+            assert schema["type"] == "string", field
+        elif type_str == "int":
+            assert schema["type"] == "integer", field
+        elif type_str == "bool":
+            assert schema["type"] == "boolean", field
+        elif type_str in ("str | None", "None | str"):
+            assert schema["type"] == ["string", "null"], field
+        elif type_str in ("int | None", "None | int"):
+            assert schema["type"] == ["integer", "null"], field
+        elif type_str in ("bool | None", "None | bool"):
+            assert schema["type"] == ["boolean", "null"], field
+        elif type_str == "list[str]":
+            assert schema["type"] == "array", field
+            assert schema["items"]["type"] == "string", field
+        elif type_str == "dict[str, str]":
+            assert schema["type"] == "object", field
+            assert schema["additionalProperties"]["type"] == "string", field
+        elif type_str.startswith("dict["):
+            assert schema["type"] == "object", field
+        else:
+            raise ValueError(f"Unknown type: {type_str!r} for {field!r}")
+
+    # check descriptions are consistent
     source = inspect.getsource(NeedsInfoType)
     klass = ast.parse(source).body[0]
     descriptions = {}