Repository: useblocks/sphinx-needs
Pull request #1337 (2024-10-25 14:13:36)

✨ Add `needs_from_toml` configuration
This configuration allows for a path to a [toml file](https://toml.io) to be specified.
In this case, needs configuration will be read from it and merged into the config from the `conf.py`.
An additional `needs_from_toml_table` config controls the toml table to read from.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/useblocks/sphinx-needs/pull/1337?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=useblocks) Report
Attention: Patch coverage is `29.54545%` with `31 lines` in your changes missing coverage. Please review.
> Project coverage is 87.63%. 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 [(`fbdb7b9`)](https://app.codecov.io/gh/useblocks/sphinx-needs/commit/fbdb7b9d2ef8eb75253da11cb15b4bae833f1bdc?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=useblocks).
> Report is 78 commits behind head on master.

| [Files with missing lines](https://app.codecov.io/gh/useblocks/sphinx-needs/pull/1337?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=useblocks) | Patch % | Lines |
|---|---|---|
| [sphinx\_needs/needs.py](https://app.codecov.io/gh/useblocks/sphinx-needs/pull/1337?src=pr&el=tree&filepath=sphinx_needs%2Fneeds.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=useblocks#diff-c3BoaW54X25lZWRzL25lZWRzLnB5) | 23.68% | [29 Missing :warning: ](https://app.codecov.io/gh/useblocks/sphinx-needs/pull/1337?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=useblocks) |
| [sphinx\_needs/config.py](https://app.codecov.io/gh/useblocks/sphinx-needs/pull/1337?src=pr&el=tree&filepath=sphinx_needs%2Fconfig.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=useblocks#diff-c3BoaW54X25lZWRzL2NvbmZpZy5weQ==) | 66.66% | [2 Missing :warning: ](https://app.codecov.io/gh/useblocks/sphinx-needs/pull/1337?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=useblocks) |

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


```diff
@@            Coverage Diff             @@
##           master    #1337      +/-   ##
==========================================
+ Coverage   86.87%   87.63%   +0.75%     
==========================================
  Files          56       60       +4     
  Lines        6532     7078     +546     
==========================================
+ Hits         5675     6203     +528     
- Misses        857      875      +18     
```

| [Flag](https://app.codecov.io/gh/useblocks/sphinx-needs/pull/1337/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/1337/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=useblocks) | `87.63% <29.54%> (+0.75%)` | :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/1337?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/docs/conf.py b/docs/conf.py
index 82d81816..7af6a7b5 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -265,6 +265,7 @@ linkcheck_ignore = [
     r"http://localhost:\d+",
     r"http://127.0.0.1:\d+",
     r"../.*",
+    r"http://sourceforge.net/projects/plantuml.*",
     r"https?://useblocks.com/sphinx-needs/bench/index.html",
 ]
 
@@ -288,339 +289,11 @@ plantuml_output_format = "svg_img"
 
 # -- Options for Needs extension ---------------------------------------
 
+needs_from_toml = "ubproject.toml"
+
 needs_debug_measurement = "READTHEDOCS" in os.environ  # run on CI
 needs_debug_filters = True
 
-needs_types = [
-    # Architecture types
-    {
-        "directive": "int",
-        "title": "Interface",
-        "content": "plantuml",
-        "prefix": "I_",
-        "color": "#BFD8D2",
-        "style": "card",
-    },
-    {
-        "directive": "comp",
-        "title": "Component",
-        "content": "plantuml",
-        "prefix": "C_",
-        "color": "#BFD8D2",
-        "style": "card",
-    },
-    {
-        "directive": "sys",
-        "title": "System",
-        "content": "plantuml",
-        "prefix": "S_",
-        "color": "#BFD8D2",
-        "style": "card",
-    },
-    # Normal types
-    {
-        "directive": "req",
-        "title": "Requirement",
-        "prefix": "R_",
-        "color": "#BFD8D2",
-        "style": "node",
-    },
-    {
-        "directive": "spec",
-        "title": "Specification",
-        "prefix": "S_",
-        "color": "#FEDCD2",
-        "style": "node",
-    },
-    {
-        "directive": "impl",
-        "title": "Implementation",
-        "prefix": "I_",
-        "color": "#DF744A",
-        "style": "node",
-    },
-    {
-        "directive": "test",
-        "title": "Test Case",
-        "prefix": "T_",
-        "color": "#DCB239",
-        "style": "node",
-    },
-    {
-        "directive": "feature",
-        "title": "Feature",
-        "prefix": "F_",
-        "color": "#FFCC00",
-        "style": "node",
-    },
-    {
-        "directive": "user",
-        "title": "User",
-        "prefix": "U_",
-        "color": "#777777",
-        "style": "node",
-    },
-    {
-        "directive": "action",
-        "title": "Action",
-        "prefix": "A_",
-        "color": "#FFCC00",
-        "style": "node",
-    },
-    {
-        "directive": "milestone",
-        "title": "Milestone",
-        "prefix": "M_",
-        "color": "#FF3333",
-        "style": "node",
-    },
-    # for tutorial
-    {
-        "directive": "tutorial-project",
-        "title": "Project",
-        "prefix": "P_",
-        "color": "#BFD8D2",
-        "style": "rectangle",
-    },
-    {
-        "directive": "tutorial-req",
-        "title": "Requirement",
-        "prefix": "R_",
-        "color": "#BFD8D2",
-        "style": "rectangle",
-    },
-    {
-        "directive": "tutorial-spec",
-        "title": "Specification",
-        "prefix": "S_",
-        "color": "#FEDCD2",
-        "style": "rectangle",
-    },
-    {
-        "directive": "tutorial-test",
-        "title": "Test Case",
-        "prefix": "T_",
-        "color": "#f9e79f",
-        "style": "rectangle",
-    },
-]
-
-needs_extra_links = [
-    {
-        "option": "blocks",
-        "incoming": "is blocked by",
-        "outgoing": "blocks",
-        "copy": True,
-        "style": "#AA0000",
-        "style_part": "dotted,#AA0000",
-        "style_start": "-",
-        "style_end": "-o",
-        "allow_dead_links": True,
-    },
-    {
-        "option": "tests",
-        "incoming": "is tested by",
-        "outgoing": "tests",
-        "copy": True,
-        "style": "#00AA00",
-        "style_part": "dotted,#00AA00",
-    },
-    {
-        "option": "checks",
-        "incoming": "is checked by",
-        "outgoing": "checks",
-        "copy": False,
-        "style": "#00AA00",
-        "style_part": "dotted,#00AA00",
-    },
-    {
-        "option": "triggers",
-        "incoming": "triggered by",
-        "outgoing": "triggers",
-        "copy": False,
-        "style": "#00AA00",
-        "style_part": "solid,#777777",
-        "allow_dead_links": True,
-    },
-    {
-        "option": "starts_with",
-        "incoming": "triggers directly",
-        "outgoing": "starts with",
-        "copy": False,
-        "style": "#00AA00",
-        "style_part": "solid,#777777",
-    },
-    {
-        "option": "starts_after",
-        "incoming": "triggers at end",
-        "outgoing": "starts after",
-        "copy": False,
-        "style": "#00AA00",
-        "style_part": "solid,#777777",
-    },
-    {
-        "option": "ends_with",
-        "incoming": "triggers to end with",
-        "outgoing": "ends with",
-        "copy": False,
-        "style": "#00AA00",
-        "style_part": "solid,#777777",
-    },
-    # for tutorial
-    {
-        "option": "tutorial_required_by",
-        "incoming": "requires",
-        "outgoing": "required by",
-        "style": "#00AA00",
-    },
-    {
-        "option": "tutorial_specifies",
-        "incoming": "specified by",
-        "outgoing": "specifies",
-    },
-    {
-        "option": "tutorial_tests",
-        "incoming": "tested by",
-        "outgoing": "tests",
-    },
-]
-
-needs_variant_options = ["status"]
-
-needs_flow_configs = {
-    "my_config": """
-       skinparam monochrome true
-       skinparam componentStyle uml2
-   """,
-    "another_config": """
-       skinparam class {
-           BackgroundColor PaleGreen
-           ArrowColor SeaGreen
-           BorderColor SpringGreen
-       }
-   """,
-    "tutorial": """
-    left to right direction
-    skinparam backgroundcolor transparent
-    skinparam Arrow {
-      Color #57ACDC
-      FontColor #808080
-      FontStyle Bold
-    }
-    skinparam rectangleBorderThickness 2
-   """,
-}
-
-needs_graphviz_styles = {
-    "tutorial": {
-        "graph": {
-            "rankdir": "LR",
-            "bgcolor": "transparent",
-        },
-        "node": {
-            "fontname": "sans-serif",
-            "fontsize": 12,
-            "penwidth": 2,
-            "margin": "0.11,0.11",
-            "style": "rounded",
-        },
-        "edge": {
-            "color": "#57ACDC",
-            "fontsize": 10,
-            "fontcolor": "#808080",
-        },
-    }
-}
-
-needs_show_link_type = False
-needs_show_link_title = False
-needs_title_optional = True
-needs_max_title_length = 75
-
-needs_id_regex = "^[A-Za-z0-9_]*"
-needs_id_required = False
-# needs_css = "dark.css"
-
-needs_table_style = "datatables"
-needs_table_columns = "ID;TITLE;STATUS;OUTGOING"
-
-needs_extra_options = [
-    "my_extra_option",
-    "another_option",
-    "author",
-    "comment",
-    "amount",
-    "hours",
-    "image",
-    "config",
-    "github",
-    "value",
-    "unit",
-]
-
-_names = [t["directive"] for t in needs_types] + ["issue", "pr", "commit"]
-needs_warnings = {
-    "type_check": f"type not in {_names}",
-    # 'valid_status': 'status not in ["open", "in progress", "closed", "done", "implemented"] and status is not None'
-}
-
-needs_default_layout = "clean"
-needs_default_style = None
-
-needs_layouts = {
-    "example": {
-        "grid": "simple_side_right_partial",
-        "layout": {
-            "head": ['**<<meta("title")>>** for *<<meta("author")>>*'],
-            "meta": [
-                '**status**: <<meta("status")>>',
-                '**author**: <<meta("author")>>',
-            ],
-            "side": ['<<image("_images/{{author}}.png", align="center")>>'],
-        },
-    },
-    "permalink_example": {
-        "grid": "simple",
-        "layout": {
-            "head": [
-                '<<meta("type_name")>>: **<<meta("title")>>** <<meta_id()>> <<permalink()>> <<collapse_button("meta", '
-                'collapsed="icon:arrow-down-circle", visible="icon:arrow-right-circle", initial=False)>> '
-            ],
-            "meta": ["<<meta_all(no_links=True)>>", "<<meta_links_all()>>"],
-        },
-    },
-    "detail_view": {
-        "grid": "simple",
-        "layout": {
-            "head": [
-                '<<meta("type_name")>>: **<<meta("title")>>** <<meta_id()>> <<permalink()>> <<collapse_button("meta", '
-                'collapsed="icon:arrow-down-circle", visible="icon:arrow-right-circle", initial=False)>> '
-                '<<sidebar("")>>'
-            ],
-            "meta": ["<<meta_all(no_links=True)>>", "<<meta_links_all()>>"],
-        },
-    },
-}
-
-needs_service_all_data = True
-
-needs_services = {}
-
-needs_string_links = {
-    "config_link": {
-        "regex": r"^(?P<value>\w+)$",
-        "link_url": 'https://sphinxcontrib-needs.readthedocs.io/en/latest/configuration.html#{{value | replace("_", "-")}}',
-        "link_name": 'Sphinx-Needs docs for {{value | replace("_", "-") }}',
-        "options": ["config"],
-    },
-    "github_link": {
-        "regex": r"^(?P<value>\w+)$",
-        "link_url": "https://github.com/useblocks/sphinxcontrib-needs/issues/{{value}}",
-        "link_name": "GitHub #{{value}}",
-        "options": ["github"],
-    },
-}
-
 
 def custom_defined_func():
     return "List of contributors:"
@@ -642,59 +315,8 @@ needs_render_context = {
 #     },
 # ]
 
-# build needs.json to make permalinks work
-needs_build_json = True
-needs_reproducible_json = True
-needs_json_remove_defaults = True
-
-# build needs_json for every needs-id to make detail panel
-needs_build_json_per_id = False
-
-# contains different constraints
-needs_constraints = {
-    "critical": {
-        "check_0": "'critical' in tags",
-        "check_1": "'SECURITY_REQ' in links",
-        "severity": "CRITICAL",
-    },
-    "security": {"check_0": "'security' in tags", "severity": "HIGH"},
-    "team": {"check_0": 'author == "Bob"', "severity": "LOW"},
-}
-
-# defines what to do if a constraint is not met
-needs_constraint_failed_options = {
-    "CRITICAL": {"on_fail": ["warn"], "style": ["red_bar"], "force_style": True},
-    "HIGH": {"on_fail": ["warn"], "style": ["orange_bar"], "force_style": True},
-    "MEDIUM": {"on_fail": ["warn"], "style": ["yellow_bar"], "force_style": False},
-    "LOW": {"on_fail": [], "style": ["yellow_bar"], "force_style": False},
-}
-
-NOTE_TEMPLATE = """
-.. _{{id}}:
-
-.. note:: {{title}} ({{id}})
-
-   {{content|indent(4) }}
-
-   {% if status -%}
-   **status**: {{status}}
-   {% endif %}
-
-   {% if tags -%}
-   **tags**: {{"; ".join(tags)}}
-   {% endif %}
-
-   {% if links -%}
-   **links**:
-   {% for link in links -%}
-       :ref:`{{link}} <{{link}}>` {%if loop.index < links|length -%}; {% endif -%}
-   {% endfor -%}
-   {% endif %}
-"""
-
-DEFAULT_DIAGRAM_TEMPLATE = "<size:12>{{type_name}}</size>\\n**{{title|wordwrap(15, wrapstring='**\\\\n**')}}**\\n<size:10>{{id}}</size>"
-
 # You can uncomment some of the following lines to override the default configuration for Sphinx-Needs.
+# DEFAULT_DIAGRAM_TEMPLATE = "<size:12>{{type_name}}</size>\\n**{{title|wordwrap(15, wrapstring='**\\\\n**')}}**\\n<size:10>{{id}}</size>"
 # needs_diagram_template = DEFAULT_DIAGRAM_TEMPLATE
 
 # Absolute path to the needs_report_template_file based on the conf.py directory
diff --git a/docs/configuration.rst b/docs/configuration.rst
index 251c164d..d699d107 100644
--- a/docs/configuration.rst
+++ b/docs/configuration.rst
@@ -3,7 +3,7 @@
 Configuration
 =============
 
-All configurations take place in your project's **conf.py** file.
+All root configurations take place in your project's :external+sphinx:ref:`conf.py file <build-config>`.
 
 Activation
 ----------
@@ -14,6 +14,13 @@ Add **sphinx_needs** to your extensions.
 
    extensions = ["sphinx_needs",]
 
+Available sphinx-needs options are then listed below, that can be added to your **conf.py** file.
+
+.. versionadded:: 4.1.0
+
+   Configuration can also be specified via a `toml file <https://toml.io>`__.
+   See :ref:`needs_from_toml` for more details.
+
 .. _config-warnings:
 
 Build Warnings
@@ -79,6 +86,42 @@ Options
 
 All configuration options starts with the prefix ``needs_`` for **Sphinx-Needs**.
 
+.. _needs_from_toml:
+
+needs_from_toml
+~~~~~~~~~~~~~~~
+
+.. versionadded:: 4.1.0
+
+This configuration takes the (relative) path to a `toml file <https://toml.io>`__ which contains some or all of the needs configuration (configuration in the toml will override that in the :file:`conf.py`).
+
+.. code-block:: python
+
+   needs_from_toml = "ubproject.toml"
+
+Configuration in the toml can contain any of the options below, under a ``[needs]`` section,
+but with the ``needs_`` prefix removed.
+For example:
+
+.. code-block:: toml
+
+   [needs]
+   id_required = true
+   id_length = 3
+   types = [
+      {directive="req", title="Requirement", prefix="R_", color="#BFD8D2", style="node"},
+      {directive="spec", title="Specification", prefix="S_", color="#FEDCD2", style="node"},
+   ]
+
+To specify a different `table path <https://toml.io/en/v1.0.0#table>`__ to read from in the toml file, use the ``needs_from_toml_table`` option.
+For example to read from a ``[tool.needs]`` table:
+
+.. code-block:: python
+
+   needs_from_toml_table = ["tool", "needs"]
+
+.. caution:: Any configuration specifying relative paths in the toml file will be resolved relative to the directory containing the :file:`conf.py` file.
+
 needs_include_needs
 ~~~~~~~~~~~~~~~~~~~
 Set this option to False, if no needs should be documented inside the generated documentation.
diff --git a/docs/contributing.rst b/docs/contributing.rst
index e1decc0f..033b7a61 100644
--- a/docs/contributing.rst
+++ b/docs/contributing.rst
@@ -117,7 +117,7 @@ Running JS Testcases with PyTest
 **Setup Cypress Locally**
 
 * Install Node JS on your computer and ensure it can be accessed through the CMD.
-* Install Cypress using the npm package manager by running ``npm install cypress``. Visit this link for more information on `how to install Cypress <https://docs.cypress.io/guides/getting-started/installing-cypress#npm-install>`_.
+* Install Cypress using the npm package manager by running ``npm install cypress``. Visit this link for more information on `how to install Cypress <https://docs.cypress.io/guides/getting-started/installing-cypress>`_.
 * Verify if Cypress is installed correctly and is executable by running: ``npx cypress verify``. Get out this page for more information about `Cypress commandline <https://docs.cypress.io/guides/guides/command-line>`_.
 * If everything is successful then we can use Cypress.
 
diff --git a/docs/ubproject.toml b/docs/ubproject.toml
new file mode 100644
index 00000000..d9e06dcb
--- /dev/null
+++ b/docs/ubproject.toml
@@ -0,0 +1,331 @@
+[needs]
+extra_options = [
+    "my_extra_option",
+    "another_option",
+    "author",
+    "comment",
+    "amount",
+    "hours",
+    "image",
+    "config",
+    "github",
+    "value",
+    "unit",
+]
+variant_options = ["status"]
+show_link_type = false
+show_link_title = false
+title_optional = true
+max_title_length = 75
+id_regex = "^[A-Za-z0-9_]*"
+id_required = false
+table_style = "datatables"
+table_columns = "ID;TITLE;STATUS;OUTGOING"
+default_layout = "clean"
+service_all_data = true
+# needs.json build options
+build_json = true
+reproducible_json = true
+json_remove_defaults = true
+build_json_per_id = false
+
+[[needs.types]]
+directive = "int"
+title = "Interface"
+content = "plantuml"
+prefix = "I_"
+color = "#BFD8D2"
+style = "card"
+
+[[needs.types]]
+directive = "comp"
+title = "Component"
+content = "plantuml"
+prefix = "C_"
+color = "#BFD8D2"
+style = "card"
+
+[[needs.types]]
+directive = "sys"
+title = "System"
+content = "plantuml"
+prefix = "S_"
+color = "#BFD8D2"
+style = "card"
+
+[[needs.types]]
+directive = "req"
+title = "Requirement"
+prefix = "R_"
+color = "#BFD8D2"
+style = "node"
+
+[[needs.types]]
+directive = "spec"
+title = "Specification"
+prefix = "S_"
+color = "#FEDCD2"
+style = "node"
+
+[[needs.types]]
+directive = "impl"
+title = "Implementation"
+prefix = "I_"
+color = "#DF744A"
+style = "node"
+
+[[needs.types]]
+directive = "test"
+title = "Test Case"
+prefix = "T_"
+color = "#DCB239"
+style = "node"
+
+[[needs.types]]
+directive = "feature"
+title = "Feature"
+prefix = "F_"
+color = "#FFCC00"
+style = "node"
+
+[[needs.types]]
+directive = "user"
+title = "User"
+prefix = "U_"
+color = "#777777"
+style = "node"
+
+[[needs.types]]
+directive = "action"
+title = "Action"
+prefix = "A_"
+color = "#FFCC00"
+style = "node"
+
+[[needs.types]]
+directive = "milestone"
+title = "Milestone"
+prefix = "M_"
+color = "#FF3333"
+style = "node"
+
+[[needs.types]]
+directive = "tutorial-project"
+title = "Project"
+prefix = "P_"
+color = "#BFD8D2"
+style = "rectangle"
+
+[[needs.types]]
+directive = "tutorial-req"
+title = "Requirement"
+prefix = "R_"
+color = "#BFD8D2"
+style = "rectangle"
+
+[[needs.types]]
+directive = "tutorial-spec"
+title = "Specification"
+prefix = "S_"
+color = "#FEDCD2"
+style = "rectangle"
+
+[[needs.types]]
+directive = "tutorial-test"
+title = "Test Case"
+prefix = "T_"
+color = "#f9e79f"
+style = "rectangle"
+
+[[needs.extra_links]]
+option = "blocks"
+incoming = "is blocked by"
+outgoing = "blocks"
+copy = true
+style = "#AA0000"
+style_part = "dotted,#AA0000"
+style_start = "-"
+style_end = "-o"
+allow_dead_links = true
+
+[[needs.extra_links]]
+option = "tests"
+incoming = "is tested by"
+outgoing = "tests"
+copy = true
+style = "#00AA00"
+style_part = "dotted,#00AA00"
+
+[[needs.extra_links]]
+option = "checks"
+incoming = "is checked by"
+outgoing = "checks"
+copy = false
+style = "#00AA00"
+style_part = "dotted,#00AA00"
+
+[[needs.extra_links]]
+option = "triggers"
+incoming = "triggered by"
+outgoing = "triggers"
+copy = false
+style = "#00AA00"
+style_part = "solid,#777777"
+allow_dead_links = true
+
+[[needs.extra_links]]
+option = "starts_with"
+incoming = "triggers directly"
+outgoing = "starts with"
+copy = false
+style = "#00AA00"
+style_part = "solid,#777777"
+
+[[needs.extra_links]]
+option = "starts_after"
+incoming = "triggers at end"
+outgoing = "starts after"
+copy = false
+style = "#00AA00"
+style_part = "solid,#777777"
+
+[[needs.extra_links]]
+option = "ends_with"
+incoming = "triggers to end with"
+outgoing = "ends with"
+copy = false
+style = "#00AA00"
+style_part = "solid,#777777"
+
+[[needs.extra_links]]
+option = "tutorial_required_by"
+incoming = "requires"
+outgoing = "required by"
+style = "#00AA00"
+
+[[needs.extra_links]]
+option = "tutorial_specifies"
+incoming = "specified by"
+outgoing = "specifies"
+
+[[needs.extra_links]]
+option = "tutorial_tests"
+incoming = "tested by"
+outgoing = "tests"
+
+[needs.warnings]
+type_check = "type not in ['int', 'comp', 'sys', 'req', 'spec', 'impl', 'test', 'feature', 'user', 'action', 'milestone', 'tutorial-project', 'tutorial-req', 'tutorial-spec', 'tutorial-test', 'issue', 'pr', 'commit']"
+
+[needs.constraints.critical]
+check_0 = "'critical' in tags"
+check_1 = "'SECURITY_REQ' in links"
+severity = "CRITICAL"
+
+[needs.constraints.security]
+check_0 = "'security' in tags"
+severity = "HIGH"
+
+[needs.constraints.team]
+check_0 = "author == \"Bob\""
+severity = "LOW"
+
+[needs.constraint_failed_options.CRITICAL]
+on_fail = ["warn"]
+style = ["red_bar"]
+force_style = true
+
+[needs.constraint_failed_options.HIGH]
+on_fail = ["warn"]
+style = ["orange_bar"]
+force_style = true
+
+[needs.constraint_failed_options.MEDIUM]
+on_fail = ["warn"]
+style = ["yellow_bar"]
+force_style = false
+
+[needs.constraint_failed_options.LOW]
+on_fail = []
+style = ["yellow_bar"]
+force_style = false
+
+[needs.flow_configs]
+my_config = """
+skinparam monochrome true
+skinparam componentStyle uml2
+"""
+another_config = """
+skinparam class {
+    BackgroundColor PaleGreen
+    ArrowColor SeaGreen
+    BorderColor SpringGreen
+}
+"""
+tutorial = """
+left to right direction
+skinparam backgroundcolor transparent
+skinparam Arrow {
+    Color #57ACDC
+    FontColor #808080
+    FontStyle Bold
+}
+skinparam rectangleBorderThickness 2
+"""
+
+[needs.graphviz_styles.tutorial.graph]
+rankdir = "LR"
+bgcolor = "transparent"
+
+[needs.graphviz_styles.tutorial.node]
+fontname = "sans-serif"
+fontsize = 12
+penwidth = 2
+margin = "0.11,0.11"
+style = "rounded"
+
+[needs.graphviz_styles.tutorial.edge]
+color = "#57ACDC"
+fontsize = 10
+fontcolor = "#808080"
+
+[needs.layouts.example]
+grid = "simple_side_right_partial"
+
+[needs.layouts.example.layout]
+head = ["**<<meta(\"title\")>>** for *<<meta(\"author\")>>*"]
+meta = ["**status**: <<meta(\"status\")>>", "**author**: <<meta(\"author\")>>"]
+side = ["<<image(\"_images/{{author}}.png\", align=\"center\")>>"]
+
+[needs.layouts.permalink_example]
+grid = "simple"
+
+[needs.layouts.permalink_example.layout]
+head = [
+    "<<meta(\"type_name\")>>: **<<meta(\"title\")>>** <<meta_id()>> <<permalink()>> <<collapse_button(\"meta\", collapsed=\"icon:arrow-down-circle\", visible=\"icon:arrow-right-circle\", initial=False)>> ",
+]
+meta = ["<<meta_all(no_links=True)>>", "<<meta_links_all()>>"]
+
+[needs.layouts.detail_view]
+grid = "simple"
+
+[needs.layouts.detail_view.layout]
+head = [
+    "<<meta(\"type_name\")>>: **<<meta(\"title\")>>** <<meta_id()>> <<permalink()>> <<collapse_button(\"meta\", collapsed=\"icon:arrow-down-circle\", visible=\"icon:arrow-right-circle\", initial=False)>> <<sidebar(\"\")>>",
+]
+meta = ["<<meta_all(no_links=True)>>", "<<meta_links_all()>>"]
+
+[needs.string_links.config_link]
+regex = "^(?P<value>\\w+)$"
+link_url = "https://sphinxcontrib-needs.readthedocs.io/en/latest/configuration.html#{{value | replace(\"_\", \"-\")}}"
+link_name = "Sphinx-Needs docs for {{value | replace(\"_\", \"-\") }}"
+options = ["config"]
+
+[needs.string_links.github_link]
+regex = "^(?P<value>\\w+)$"
+link_url = "https://github.com/useblocks/sphinxcontrib-needs/issues/{{value}}"
+link_name = "GitHub #{{value}}"
+options = ["github"]
+
+[needs_json]
+path = "_build/html/furo/needs.json"
+src = "."
diff --git a/pyproject.toml b/pyproject.toml
index 9adf4a67..5a31ee75 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -39,6 +39,7 @@ requests = "^2.32"  # external_links
 jsonschema = ">=3.2.0"  # needsimport schema validation
 sphinx-data-viewer = "^0.1.5"  # needservice debug output
 sphinxcontrib-jquery = "^4"  # needed for datatables in sphinx>=6
+tomli = { version = "*", python = "<3.11" }
 
 # [project.optional-dependencies.plotting]
 # for needpie / needbar
@@ -149,6 +150,8 @@ module = [
   "requests_file",
   "sphinx_data_viewer.*",
   "sphinxcontrib.plantuml.*",
+  "tomllib.*",
+  "tomli.*",
 ]
 ignore_missing_imports = true
 
@@ -167,6 +170,11 @@ module = [
 ]
 disable_error_code = ["attr-defined", "no-any-return"]
 
+[[tool.mypy.overrides]]
+module = ["sphinx_needs.needs"]
+# issue with importing tomllib/tomli based on python version
+disable_error_code = ["no-redef"]
+
 [build-system]
 requires = ["setuptools", "poetry_core>=1.0.8"]  # setuptools for deps like plantuml
 build-backend = "poetry.core.masonry.api"
diff --git a/sphinx_needs/config.py b/sphinx_needs/config.py
index 5d4f055b..1a852407 100644
--- a/sphinx_needs/config.py
+++ b/sphinx_needs/config.py
@@ -291,6 +291,12 @@ class NeedsSphinxConfig:
                 types=item.metadata["types"],
             )
 
+    @classmethod
+    def field_names(cls) -> set[str]:
+        """Get all config fields (without ``needs_`` prefix)"""
+        names = [field.name for field in fields(cls)]
+        return {name[1:] if name.startswith("_") else name for name in names}
+
     @classmethod
     def get_default(cls, name: str) -> Any:
         """Get the default value for a config item."""
@@ -301,6 +307,16 @@ class NeedsSphinxConfig:
             return _field.default_factory()
         return _field.default
 
+    from_toml: str | None = field(
+        default=None, metadata={"rebuild": "env", "types": (str, type(None))}
+    )
+    """Path to a TOML file to load configuration from."""
+
+    from_toml_table: list[str] = field(
+        default_factory=lambda: ["needs"], metadata={"rebuild": "env", "types": (list,)}
+    )
+    """Path to the table in the toml file to load configuration from."""
+
     types: list[NeedType] = field(
         default_factory=lambda: [
             {
diff --git a/sphinx_needs/needs.py b/sphinx_needs/needs.py
index d5665964..e54fcd2e 100644
--- a/sphinx_needs/needs.py
+++ b/sphinx_needs/needs.py
@@ -112,6 +112,11 @@ from sphinx_needs.services.open_needs import OpenNeedsService
 from sphinx_needs.utils import node_match
 from sphinx_needs.warnings import process_warnings
 
+try:
+    import tomllib  # added in python 3.11
+except ImportError:
+    import tomli as tomllib
+
 __version__ = VERSION = "4.0.0"
 
 _NODE_TYPES_T = Dict[
@@ -256,6 +261,7 @@ def setup(app: Sphinx) -> dict[str, Any]:
     # EVENTS
     ########################################################################
     # Make connections to events
+    app.connect("config-inited", load_config_from_toml, priority=10)  # runs early
     app.connect("config-inited", load_config)
     app.connect("config-inited", check_configuration)
 
@@ -356,6 +362,48 @@ def process_creator(
     return process_caller
 
 
+def load_config_from_toml(app: Sphinx, config: Config) -> None:
+    """
+    Load config from toml file, if defined in conf.py
+    """
+    needs_config = NeedsSphinxConfig(config)
+    if needs_config.from_toml is None:
+        return
+
+    # resolve relative to confdir
+    toml_file = Path(app.confdir, needs_config.from_toml).resolve()
+    toml_path = needs_config.from_toml_table
+
+    if not toml_file.exists():
+        log_warning(
+            LOGGER,
+            f"'needs_from_toml' file does not exist: {toml_file}",
+            "config",
+            None,
+        )
+        return
+    try:
+        with toml_file.open("rb") as f:
+            toml_data = tomllib.load(f)
+        for key in toml_path:
+            toml_data = toml_data[key]
+        assert isinstance(toml_data, dict), "Data must be a dict"
+    except Exception as e:
+        log_warning(
+            LOGGER,
+            f"Error loading 'needs_from_toml' file: {e}",
+            "config",
+            None,
+        )
+        return
+
+    allowed_keys = NeedsSphinxConfig.field_names()
+    for key, value in toml_data.items():
+        if key not in allowed_keys:
+            continue
+        config["needs_" + key] = value
+
+
 def load_config(app: Sphinx, *_args: Any) -> None:
     """
     Register extra options and directive based on config from conf.py


Test patch:
diff --git a/tests/__snapshots__/test_needs_from_toml.ambr b/tests/__snapshots__/test_needs_from_toml.ambr
new file mode 100644
index 00000000..dd3933cf
--- /dev/null
+++ b/tests/__snapshots__/test_needs_from_toml.ambr
@@ -0,0 +1,29 @@
+# name: test_needs_from_toml[test_app0]
+  dict({
+    'current_version': '0.1.0',
+    'versions': dict({
+      '0.1.0': dict({
+        'needs': dict({
+          'O_001': dict({
+            'custom': 'custom value',
+            'docname': 'index',
+            'external_css': 'external_link',
+            'full_title': 'Test need',
+            'id': 'O_001',
+            'layout': '',
+            'section_name': 'TEST DOCUMENT',
+            'sections': list([
+              'TEST DOCUMENT',
+            ]),
+            'status': 'open',
+            'title': 'Test need',
+            'type': 'other',
+            'type_name': 'Another need type',
+          }),
+        }),
+        'needs_amount': 1,
+        'needs_defaults_removed': True,
+      }),
+    }),
+  })
+# ---
diff --git a/tests/doc_test/needs_from_toml/a.toml b/tests/doc_test/needs_from_toml/a.toml
new file mode 100644
index 00000000..764d855b
--- /dev/null
+++ b/tests/doc_test/needs_from_toml/a.toml
@@ -0,0 +1,9 @@
+[needs]
+id_regex = "^[A-Za-z0-9_]"
+types = [
+    { directive = "other", title = "Another need type", prefix = "O_", color = "#DCB239" },
+]
+extra_options = ["custom"]
+build_json = true
+reproducible_json = true
+json_remove_defaults = true
diff --git a/tests/doc_test/needs_from_toml/conf.py b/tests/doc_test/needs_from_toml/conf.py
new file mode 100644
index 00000000..b109dec9
--- /dev/null
+++ b/tests/doc_test/needs_from_toml/conf.py
@@ -0,0 +1,7 @@
+project = "needs"
+version = "0.1.0"
+copyright = "2024"
+
+extensions = ["sphinx_needs"]
+
+needs_from_toml = "a.toml"
diff --git a/tests/doc_test/needs_from_toml/index.rst b/tests/doc_test/needs_from_toml/index.rst
new file mode 100644
index 00000000..830ad993
--- /dev/null
+++ b/tests/doc_test/needs_from_toml/index.rst
@@ -0,0 +1,7 @@
+TEST DOCUMENT
+=============
+
+.. other:: Test need
+   :id: O_001
+   :status: open
+   :custom: custom value
diff --git a/tests/test_needs_from_toml.py b/tests/test_needs_from_toml.py
new file mode 100644
index 00000000..a45a4880
--- /dev/null
+++ b/tests/test_needs_from_toml.py
@@ -0,0 +1,26 @@
+import json
+from pathlib import Path
+
+import pytest
+from syrupy.filters import props
+
+
+@pytest.mark.parametrize(
+    "test_app",
+    [
+        {
+            "buildername": "html",
+            "srcdir": "doc_test/needs_from_toml",
+            "no_plantuml": True,
+        }
+    ],
+    indirect=True,
+)
+def test_needs_from_toml(test_app, snapshot):
+    app = test_app
+    app.build()
+    assert not app._warning.getvalue()
+    data = json.loads(Path(app.outdir, "needs.json").read_text("utf8"))
+    assert data == snapshot(
+        exclude=props("created", "project", "creator", "needs_schema")
+    )