From 3d439ecf1364e03da8129fd649a27683a7951a0f Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Sun, 27 Oct 2024 16:01:25 +0000 Subject: [PATCH 1/5] =?UTF-8?q?=E2=9C=A8=20Allow=20configuring=20descripti?= =?UTF-8?q?on=20of=20extra=20option?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/configuration.rst | 14 + sphinx_needs/config.py | 10 +- sphinx_needs/needs.py | 15 +- tests/__snapshots__/test_extra_options.ambr | 505 ++++++++++++++++++++ tests/doc_test/extra_options/conf.py | 11 +- tests/test_extra_options.py | 20 +- 6 files changed, 562 insertions(+), 13 deletions(-) create mode 100644 tests/__snapshots__/test_extra_options.ambr diff --git a/docs/configuration.rst b/docs/configuration.rst index d699d1079..95569be2b 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -256,6 +256,20 @@ And use it like: .. needlist:: :filter: "filter_me" in another_option +.. versionadded:: 4.1.0 + + Values in the list can also be dictionaries, allowing for setting a description of an option + that will be output in the schema of the :ref:`needs.json `. + + For example: + + .. code-block:: python + + needs_extra_options = [ + "my_extra_option", + {"name": "my_other_option", "description": "This is a description of the option"} + ] + .. _needs_global_options: needs_global_options diff --git a/sphinx_needs/config.py b/sphinx_needs/config.py index 1a8524076..a9c6afb5a 100644 --- a/sphinx_needs/config.py +++ b/sphinx_needs/config.py @@ -228,6 +228,14 @@ class NeedType(TypedDict): """The default node style to use in diagrams (default: "node").""" +class NeedExtraOption(TypedDict): + """Defines an extra option for needs""" + + name: str + description: NotRequired[str] + """A description of the option.""" + + @dataclass class NeedsSphinxConfig: """A wrapper around the Sphinx configuration, @@ -407,7 +415,7 @@ def get_default(cls, name: str) -> Any: default=30, metadata={"rebuild": "html", "types": (int,)} ) """Maximum length of the title in the need role output.""" - _extra_options: list[str] = field( + _extra_options: list[str | NeedExtraOption] = field( default_factory=list, metadata={"rebuild": "html", "types": (list,)} ) """List of extra options for needs, that get added as directive options and need fields.""" diff --git a/sphinx_needs/needs.py b/sphinx_needs/needs.py index e54fcd2ee..e5f083fd3 100644 --- a/sphinx_needs/needs.py +++ b/sphinx_needs/needs.py @@ -417,9 +417,18 @@ def load_config(app: Sphinx, *_args: Any) -> None: ) for option in needs_config._extra_options: - _NEEDS_CONFIG.add_extra_option( - option, "Added by needs_extra_options config", override=True - ) + description = "Added by needs_extra_options config" + if isinstance(option, str): + name = option + elif isinstance(option, dict): + name = option["name"] + description = option.get("description", description) + else: + raise NeedsConfigException( + f"Extra option {option} is not a string or dict." + ) + + _NEEDS_CONFIG.add_extra_option(name, description, override=True) # ensure options for ``needgantt`` functionality are added to the extra options for option in (needs_config.duration_option, needs_config.completion_option): diff --git a/tests/__snapshots__/test_extra_options.ambr b/tests/__snapshots__/test_extra_options.ambr new file mode 100644 index 000000000..e07ea2c3b --- /dev/null +++ b/tests/__snapshots__/test_extra_options.ambr @@ -0,0 +1,505 @@ +# name: test_custom_attributes_appear[test_app0] + dict({ + 'current_version': '', + 'versions': dict({ + '': dict({ + 'needs': dict({ + 'R_12345': dict({ + 'content': 'The Tool **shall** have a command line interface.', + 'docname': 'index', + 'external_css': 'external_link', + 'full_title': 'Command line interface', + 'id': 'R_12345', + 'impacts': 'component_a', + 'introduced': '1.0.0', + 'layout': '', + 'section_name': 'Section 1', + 'sections': list([ + 'Section 1', + 'TEST DOCUMENT', + ]), + 'tags': list([ + 'test', + 'test2', + ]), + 'title': 'Command line interface', + 'type': 'req', + 'type_name': 'Requirement', + 'updated': '1.5.1', + }), + 'R_12346': dict({ + 'content': 'The Tool **shall** have a command line interface.', + 'docname': 'index', + 'external_css': 'external_link', + 'full_title': 'Another Requirement', + 'id': 'R_12346', + 'impacts': 'component_b', + 'introduced': '1.1.1', + 'layout': '', + 'section_name': 'Section 1', + 'sections': list([ + 'Section 1', + 'TEST DOCUMENT', + ]), + 'tags': list([ + 'test', + 'test2', + ]), + 'title': 'Another Requirement', + 'type': 'req', + 'type_name': 'Requirement', + 'updated': '1.4.0', + }), + }), + 'needs_amount': 2, + 'needs_defaults_removed': True, + 'needs_schema': dict({ + '$schema': 'http://json-schema.org/draft-07/schema#', + 'properties': dict({ + 'arch': dict({ + 'additionalProperties': dict({ + 'type': 'string', + }), + 'default': dict({ + }), + 'description': 'Mapping of uml key to uml content.', + 'field_type': 'core', + 'type': 'object', + }), + 'avatar': dict({ + 'default': '', + 'description': 'Added by service github-issues', + 'field_type': 'extra', + 'type': 'string', + }), + 'closed_at': dict({ + 'default': '', + 'description': 'Added by service github-issues', + 'field_type': 'extra', + 'type': 'string', + }), + 'completion': dict({ + 'default': '', + 'description': 'Added for needgantt functionality', + 'field_type': 'extra', + 'type': 'string', + }), + 'constraints': dict({ + 'default': list([ + ]), + 'description': 'List of constraint names, which are defined for this need.', + 'field_type': 'core', + 'items': dict({ + 'type': 'string', + }), + 'type': 'array', + }), + 'constraints_error': dict({ + 'default': '', + 'description': 'An error message set if any constraint failed, and `error_message` field is set in config.', + 'field_type': 'core', + 'type': 'string', + }), + 'constraints_passed': dict({ + 'default': True, + 'description': 'True if all constraints passed, False if any failed, None if not yet checked.', + 'field_type': 'core', + 'type': 'boolean', + }), + 'constraints_results': dict({ + 'additionalProperties': dict({ + 'type': 'object', + }), + 'default': dict({ + }), + 'description': 'Mapping of constraint name, to check name, to result.', + 'field_type': 'core', + 'type': 'object', + }), + 'content': dict({ + 'default': '', + 'description': 'Content of the need.', + 'field_type': 'core', + 'type': 'string', + }), + 'created_at': dict({ + 'default': '', + 'description': 'Added by service github-issues', + 'field_type': 'extra', + 'type': 'string', + }), + 'delete': dict({ + 'default': False, + 'description': 'If true, the need is deleted entirely.', + 'field_type': 'core', + 'type': 'boolean', + }), + 'docname': dict({ + 'default': None, + 'description': 'Name of the document where the need is defined (None if external).', + 'field_type': 'core', + 'type': list([ + 'string', + 'null', + ]), + }), + 'doctype': dict({ + 'default': '.rst', + 'description': "Type of the document where the need is defined, e.g. '.rst'.", + 'field_type': 'core', + 'type': 'string', + }), + 'duration': dict({ + 'default': '', + 'description': 'Added for needgantt functionality', + 'field_type': 'extra', + 'type': 'string', + }), + 'external_css': dict({ + 'default': '', + 'description': 'CSS class name, added to the external reference.', + 'field_type': 'core', + 'type': 'string', + }), + 'external_url': dict({ + 'default': None, + 'description': 'URL of the need, if it is an external need.', + 'field_type': 'core', + 'type': list([ + 'string', + 'null', + ]), + }), + 'full_title': dict({ + 'default': '', + 'description': 'Title of the need, of unlimited length.', + 'field_type': 'core', + 'type': 'string', + }), + 'has_dead_links': dict({ + 'default': False, + 'description': 'True if any links reference need ids that are not found in the need list.', + 'field_type': 'core', + 'type': 'boolean', + }), + 'has_forbidden_dead_links': dict({ + 'default': False, + 'description': 'True if any links reference need ids that are not found in the need list, and the link type does not allow dead links.', + 'field_type': 'core', + 'type': 'boolean', + }), + 'id': dict({ + 'description': 'ID of the data.', + 'field_type': 'core', + 'type': 'string', + }), + 'id_prefix': dict({ + 'default': '', + 'description': 'Added by service github-issues', + 'field_type': 'extra', + 'type': 'string', + }), + 'impacts': dict({ + 'default': '', + 'description': 'What is the impact of this need?', + 'field_type': 'extra', + 'type': 'string', + }), + 'introduced': dict({ + 'default': '', + 'description': 'Added by needs_extra_options config', + 'field_type': 'extra', + 'type': 'string', + }), + 'is_external': dict({ + 'default': False, + 'description': 'If true, no node is created and need is referencing external url.', + 'field_type': 'core', + 'type': 'boolean', + }), + 'is_modified': dict({ + 'default': False, + 'description': 'Whether the need was modified by needextend.', + 'field_type': 'core', + 'type': 'boolean', + }), + 'is_need': dict({ + 'default': True, + 'description': 'Whether the need is a need.', + 'field_type': 'core', + 'type': 'boolean', + }), + 'is_part': dict({ + 'default': False, + 'description': 'Whether the need is a part.', + 'field_type': 'core', + 'type': 'boolean', + }), + 'jinja_content': dict({ + 'default': False, + 'description': 'Whether the content should be pre-processed by jinja.', + 'field_type': 'core', + 'type': 'boolean', + }), + 'layout': dict({ + 'default': None, + 'description': 'Key of the layout, which is used to render the need.', + 'field_type': 'core', + 'type': list([ + 'string', + 'null', + ]), + }), + 'links': dict({ + 'default': list([ + ]), + 'description': 'Link field', + 'field_type': 'links', + 'items': dict({ + 'type': 'string', + }), + 'type': 'array', + }), + 'links_back': dict({ + 'default': list([ + ]), + 'description': 'Backlink field', + 'field_type': 'backlinks', + 'items': dict({ + 'type': 'string', + }), + 'type': 'array', + }), + 'max_amount': dict({ + 'default': '', + 'description': 'Added by service github-issues', + 'field_type': 'extra', + 'type': 'string', + }), + 'max_content_lines': dict({ + 'default': '', + 'description': 'Added by service github-issues', + 'field_type': 'extra', + 'type': 'string', + }), + 'modifications': dict({ + 'default': 0, + 'description': 'Number of modifications by needextend.', + 'field_type': 'core', + 'type': 'integer', + }), + 'modified': dict({ + 'default': '', + 'description': 'When was this need last modified?', + 'field_type': 'extra', + 'type': 'string', + }), + 'params': dict({ + 'default': '', + 'description': 'Added by service open-needs', + 'field_type': 'extra', + 'type': 'string', + }), + 'parent_need': dict({ + 'default': '', + 'description': 'Simply the first parent id.', + 'field_type': 'core', + 'type': 'string', + }), + 'parent_needs': dict({ + 'default': list([ + ]), + 'description': 'Link field', + 'field_type': 'links', + 'items': dict({ + 'type': 'string', + }), + 'type': 'array', + }), + 'parent_needs_back': dict({ + 'default': list([ + ]), + 'description': 'Backlink field', + 'field_type': 'backlinks', + 'items': dict({ + 'type': 'string', + }), + 'type': 'array', + }), + 'parts': dict({ + 'additionalProperties': dict({ + 'type': 'object', + }), + 'default': dict({ + }), + 'description': "Mapping of parts, a.k.a. sub-needs, IDs to data that overrides the need's data", + 'field_type': 'core', + 'type': 'object', + }), + 'post_content': dict({ + 'default': '', + 'description': 'Post-content of the need.', + 'field_type': 'core', + 'type': 'string', + }), + 'post_template': dict({ + 'default': None, + 'description': 'Post-template of the need.', + 'field_type': 'core', + 'type': list([ + 'string', + 'null', + ]), + }), + 'pre_content': dict({ + 'default': '', + 'description': 'Pre-content of the need.', + 'field_type': 'core', + 'type': 'string', + }), + 'pre_template': dict({ + 'default': None, + 'description': 'Pre-template of the need.', + 'field_type': 'core', + 'type': list([ + 'string', + 'null', + ]), + }), + 'prefix': dict({ + 'default': '', + 'description': 'Added by service open-needs', + 'field_type': 'extra', + 'type': 'string', + }), + 'query': dict({ + 'default': '', + 'description': 'Added by service github-issues', + 'field_type': 'extra', + 'type': 'string', + }), + 'section_name': dict({ + 'default': '', + 'description': 'Simply the first section.', + 'field_type': 'core', + 'type': 'string', + }), + 'sections': dict({ + 'default': list([ + ]), + 'description': 'Sections of the need.', + 'field_type': 'core', + 'items': dict({ + 'type': 'string', + }), + 'type': 'array', + }), + 'service': dict({ + 'default': '', + 'description': 'Added by service github-issues', + 'field_type': 'extra', + 'type': 'string', + }), + 'signature': dict({ + 'default': '', + 'description': 'Derived from a docutils desc_name node.', + 'field_type': 'core', + 'type': 'string', + }), + 'specific': dict({ + 'default': '', + 'description': 'Added by service github-issues', + 'field_type': 'extra', + 'type': 'string', + }), + 'status': dict({ + 'default': None, + 'description': 'Status of the need.', + 'field_type': 'core', + 'type': list([ + 'string', + 'null', + ]), + }), + 'style': dict({ + 'default': None, + 'description': 'Comma-separated list of CSS classes (all appended by `needs_style_`).', + 'field_type': 'core', + 'type': list([ + 'string', + 'null', + ]), + }), + 'tags': dict({ + 'default': list([ + ]), + 'description': 'List of tags.', + 'field_type': 'core', + 'items': dict({ + 'type': 'string', + }), + 'type': 'array', + }), + 'template': dict({ + 'default': None, + 'description': 'Template of the need.', + 'field_type': 'core', + 'type': list([ + 'string', + 'null', + ]), + }), + 'title': dict({ + 'description': 'Title of the need, trimmed to a maximum length.', + 'field_type': 'core', + 'type': 'string', + }), + 'type': dict({ + 'default': '', + 'description': 'Type of the need.', + 'field_type': 'core', + 'type': 'string', + }), + 'type_name': dict({ + 'default': '', + 'description': 'Name of the type.', + 'field_type': 'core', + 'type': 'string', + }), + 'updated': dict({ + 'default': '', + 'description': 'Added by needs_extra_options config', + 'field_type': 'extra', + 'type': 'string', + }), + 'updated_at': dict({ + 'default': '', + 'description': 'Added by service github-issues', + 'field_type': 'extra', + 'type': 'string', + }), + 'url': dict({ + 'default': '', + 'description': 'Added by service github-issues', + 'field_type': 'extra', + 'type': 'string', + }), + 'url_postfix': dict({ + 'default': '', + 'description': 'Added by service open-needs', + 'field_type': 'extra', + 'type': 'string', + }), + 'user': dict({ + 'default': '', + 'description': 'Added by service github-issues', + 'field_type': 'extra', + 'type': 'string', + }), + }), + 'type': 'object', + }), + }), + }), + }) +# --- diff --git a/tests/doc_test/extra_options/conf.py b/tests/doc_test/extra_options/conf.py index cb7c3c6a6..e0d6e07ba 100644 --- a/tests/doc_test/extra_options/conf.py +++ b/tests/doc_test/extra_options/conf.py @@ -1,12 +1,21 @@ extensions = ["sphinx_needs"] -needs_extra_options = ["introduced", "updated", "impacts"] +needs_extra_options = [ + "introduced", + "updated", + {"name": "impacts", "description": "What is the impact of this need?"}, +] + +needs_build_json = True +needs_reproducible_json = True +needs_json_remove_defaults = True def setup(app): from sphinx_needs.api.configuration import add_extra_option add_extra_option(app, "introduced") + add_extra_option(app, "modified", description="When was this need last modified?") needs_template_collapse = """ diff --git a/tests/test_extra_options.py b/tests/test_extra_options.py index 4e6921887..86759f78f 100644 --- a/tests/test_extra_options.py +++ b/tests/test_extra_options.py @@ -1,26 +1,30 @@ +import json import re from pathlib import Path import pytest +from sphinx.util.console import strip_colors +from syrupy.filters import props @pytest.mark.parametrize( "test_app", - [{"buildername": "html", "srcdir": "doc_test/extra_options"}], + [{"buildername": "html", "srcdir": "doc_test/extra_options", "no_plantuml": True}], indirect=True, ) -def test_custom_attributes_appear(test_app): +def test_custom_attributes_appear(test_app, snapshot): app = test_app app.build() - html = Path(app.outdir, "index.html").read_text() + warnings = strip_colors(app._warning.getvalue()).splitlines() + assert warnings == [ + 'WARNING: extra_option "introduced" already registered. [needs.config]' + ] - warning = app._warning - # stdout warnings - warnings = warning.getvalue() + needs = json.loads(Path(app.outdir, "needs.json").read_text("utf8")) + assert needs == snapshot(exclude=props("created", "project", "creator")) - # Check for multiple registered names - assert 'extra_option "introduced" already registered.' in warnings + html = Path(app.outdir, "index.html").read_text() # Custom options should appear # assert 'introduced: 1.0.0' in html From 85553971a7d9320917d130993437f2b92b02d390 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Sun, 27 Oct 2024 16:25:38 +0000 Subject: [PATCH 2/5] Update needs.py --- sphinx_needs/needs.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/sphinx_needs/needs.py b/sphinx_needs/needs.py index e5f083fd3..d5b8ca1d9 100644 --- a/sphinx_needs/needs.py +++ b/sphinx_needs/needs.py @@ -421,11 +421,16 @@ def load_config(app: Sphinx, *_args: Any) -> None: if isinstance(option, str): name = option elif isinstance(option, dict): - name = option["name"] + try: + name = option["name"] + except KeyError: + raise NeedsConfigException( + f"Extra option is a dict, but does not contain a 'name' key: {option}" + ) description = option.get("description", description) else: raise NeedsConfigException( - f"Extra option {option} is not a string or dict." + f"Extra option is not a string or dict: {option}" ) _NEEDS_CONFIG.add_extra_option(name, description, override=True) From 63c64f10e68b0382afe9ea3c6469b2c71167c0d4 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Sun, 27 Oct 2024 16:28:55 +0000 Subject: [PATCH 3/5] Update needs.py --- sphinx_needs/needs.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/sphinx_needs/needs.py b/sphinx_needs/needs.py index d5b8ca1d9..da129ce3d 100644 --- a/sphinx_needs/needs.py +++ b/sphinx_needs/needs.py @@ -411,9 +411,12 @@ def load_config(app: Sphinx, *_args: Any) -> None: needs_config = NeedsSphinxConfig(app.config) if isinstance(needs_config._extra_options, dict): - LOGGER.info( + log_warning( + LOGGER, 'Config option "needs_extra_options" supports list and dict. However new default type since ' - "Sphinx-Needs 0.7.2 is list. Please see docs for details." + "Sphinx-Needs 0.7.2 is list. Please see docs for details.", + "config", + None, ) for option in needs_config._extra_options: @@ -424,14 +427,22 @@ def load_config(app: Sphinx, *_args: Any) -> None: try: name = option["name"] except KeyError: - raise NeedsConfigException( - f"Extra option is a dict, but does not contain a 'name' key: {option}" + log_warning( + LOGGER, + f"Extra option is a dict, but does not contain a 'name' key: {option}", + "config", + None, ) + continue description = option.get("description", description) else: - raise NeedsConfigException( - f"Extra option is not a string or dict: {option}" + log_warning( + LOGGER, + f"Extra option is not a string or dict: {option}", + "config", + None, ) + continue _NEEDS_CONFIG.add_extra_option(name, description, override=True) From 4c07eb8841456c8ed76a0a45e3821d03f6c9839a Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Sun, 27 Oct 2024 16:35:08 +0000 Subject: [PATCH 4/5] increase test coverage --- sphinx_needs/needs.py | 4 ++-- tests/doc_test/extra_options/conf.py | 2 ++ tests/test_extra_options.py | 4 +++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/sphinx_needs/needs.py b/sphinx_needs/needs.py index da129ce3d..e5a19d1fd 100644 --- a/sphinx_needs/needs.py +++ b/sphinx_needs/needs.py @@ -429,7 +429,7 @@ def load_config(app: Sphinx, *_args: Any) -> None: except KeyError: log_warning( LOGGER, - f"Extra option is a dict, but does not contain a 'name' key: {option}", + f"extra_option is a dict, but does not contain a 'name' key: {option}", "config", None, ) @@ -438,7 +438,7 @@ def load_config(app: Sphinx, *_args: Any) -> None: else: log_warning( LOGGER, - f"Extra option is not a string or dict: {option}", + f"extra_option is not a string or dict: {option}", "config", None, ) diff --git a/tests/doc_test/extra_options/conf.py b/tests/doc_test/extra_options/conf.py index e0d6e07ba..52b0b468b 100644 --- a/tests/doc_test/extra_options/conf.py +++ b/tests/doc_test/extra_options/conf.py @@ -4,6 +4,8 @@ "introduced", "updated", {"name": "impacts", "description": "What is the impact of this need?"}, + {}, + 1, ] needs_build_json = True diff --git a/tests/test_extra_options.py b/tests/test_extra_options.py index 86759f78f..346395ec1 100644 --- a/tests/test_extra_options.py +++ b/tests/test_extra_options.py @@ -18,7 +18,9 @@ def test_custom_attributes_appear(test_app, snapshot): warnings = strip_colors(app._warning.getvalue()).splitlines() assert warnings == [ - 'WARNING: extra_option "introduced" already registered. [needs.config]' + 'WARNING: extra_option "introduced" already registered. [needs.config]', + "WARNING: extra_option is a dict, but does not contain a 'name' key: {} [needs.config]", + "WARNING: extra_option is not a string or dict: 1 [needs.config]", ] needs = json.loads(Path(app.outdir, "needs.json").read_text("utf8")) From 0c9fd3c90f8450131c1ec1f98d523177e37a8cb2 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Mon, 28 Oct 2024 11:34:51 +0000 Subject: [PATCH 5/5] update --- docs/configuration.rst | 12 ++++++++---- sphinx_needs/config.py | 4 ++-- sphinx_needs/needs.py | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/docs/configuration.rst b/docs/configuration.rst index 95569be2b..02f5a116d 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -113,12 +113,12 @@ For example: {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. +To specify a different `root 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"] + needs_from_toml_table = ["tool"] .. caution:: Any configuration specifying relative paths in the toml file will be resolved relative to the directory containing the :file:`conf.py` file. @@ -258,8 +258,12 @@ And use it like: .. versionadded:: 4.1.0 - Values in the list can also be dictionaries, allowing for setting a description of an option - that will be output in the schema of the :ref:`needs.json `. + Values in the list can also be dictionaries, with keys: + + * ``name``: The name of the option (required). + * ``description``: A description of the option (optional). + This will be output in the schema of the :ref:`needs.json `, + and can be used by other tools. For example: diff --git a/sphinx_needs/config.py b/sphinx_needs/config.py index a9c6afb5a..c1afa03f8 100644 --- a/sphinx_needs/config.py +++ b/sphinx_needs/config.py @@ -321,9 +321,9 @@ def get_default(cls, name: str) -> Any: """Path to a TOML file to load configuration from.""" from_toml_table: list[str] = field( - default_factory=lambda: ["needs"], metadata={"rebuild": "env", "types": (list,)} + default_factory=list, metadata={"rebuild": "env", "types": (list,)} ) - """Path to the table in the toml file to load configuration from.""" + """Path to the root 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 e5a19d1fd..23577e102 100644 --- a/sphinx_needs/needs.py +++ b/sphinx_needs/needs.py @@ -385,7 +385,7 @@ def load_config_from_toml(app: Sphinx, config: Config) -> None: try: with toml_file.open("rb") as f: toml_data = tomllib.load(f) - for key in toml_path: + for key in (*toml_path, "needs"): toml_data = toml_data[key] assert isinstance(toml_data, dict), "Data must be a dict" except Exception as e: