diff --git a/docs/conf.py b/docs/conf.py
index 82d81816c..7af6a7b5a 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -265,6 +265,7 @@
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 @@
# -- 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": ['**<>** for *<>*'],
- "meta": [
- '**status**: <>',
- '**author**: <>',
- ],
- "side": ['<>'],
- },
- },
- "permalink_example": {
- "grid": "simple",
- "layout": {
- "head": [
- '<>: **<>** <> <> <> '
- ],
- "meta": ["<>", "<>"],
- },
- },
- "detail_view": {
- "grid": "simple",
- "layout": {
- "head": [
- '<>: **<>** <> <> <> '
- '<>'
- ],
- "meta": ["<>", "<>"],
- },
- },
-}
-
-needs_service_all_data = True
-
-needs_services = {}
-
-needs_string_links = {
- "config_link": {
- "regex": r"^(?P\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\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 @@ def custom_defined_func():
# },
# ]
-# 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 = "{{type_name}}\\n**{{title|wordwrap(15, wrapstring='**\\\\n**')}}**\\n{{id}}"
-
# You can uncomment some of the following lines to override the default configuration for Sphinx-Needs.
+# DEFAULT_DIAGRAM_TEMPLATE = "{{type_name}}\\n**{{title|wordwrap(15, wrapstring='**\\\\n**')}}**\\n{{id}}"
# 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 251c164db..d699d1079 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 `.
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 `__.
+ 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 `__ 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 `__ 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 e1decc0f2..033b7a610 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 `_.
+* Install Cypress using the npm package manager by running ``npm install cypress``. Visit this link for more information on `how to install 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 `_.
* If everything is successful then we can use Cypress.
diff --git a/docs/ubproject.toml b/docs/ubproject.toml
new file mode 100644
index 000000000..d9e06dcbe
--- /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 = ["**<>** for *<>*"]
+meta = ["**status**: <>", "**author**: <>"]
+side = ["<>"]
+
+[needs.layouts.permalink_example]
+grid = "simple"
+
+[needs.layouts.permalink_example.layout]
+head = [
+ "<>: **<>** <> <> <> ",
+]
+meta = ["<>", "<>"]
+
+[needs.layouts.detail_view]
+grid = "simple"
+
+[needs.layouts.detail_view.layout]
+head = [
+ "<>: **<>** <> <> <> <>",
+]
+meta = ["<>", "<>"]
+
+[needs.string_links.config_link]
+regex = "^(?P\\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\\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 9adf4a675..5a31ee75a 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 5d4f055b9..1a8524076 100644
--- a/sphinx_needs/config.py
+++ b/sphinx_needs/config.py
@@ -291,6 +291,12 @@ def add_config_values(cls, app: Sphinx) -> None:
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 @@ def get_default(cls, name: str) -> Any:
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 d56659649..e54fcd2ee 100644
--- a/sphinx_needs/needs.py
+++ b/sphinx_needs/needs.py
@@ -112,6 +112,11 @@
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_caller(app: Sphinx, doctree: nodes.document, fromdocname: str) -> No
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
diff --git a/tests/__snapshots__/test_needs_from_toml.ambr b/tests/__snapshots__/test_needs_from_toml.ambr
new file mode 100644
index 000000000..dd3933cfc
--- /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 000000000..764d855b2
--- /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 000000000..b109dec93
--- /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 000000000..830ad9932
--- /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 000000000..a45a4880b
--- /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")
+ )