From 73492f1f91d9e82efb55453a28f2788d516ed407 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 20:17:59 +0000 Subject: [PATCH 1/4] Initial plan From 7d1c5af4bcf9319ea215821929d16a56ecea8c4e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 20:24:30 +0000 Subject: [PATCH 2/4] Fix ruff format for parse_conditions changes Co-authored-by: chrisjsewell <2997570+chrisjsewell@users.noreply.github.com> --- docs/configuration.rst | 4 ++++ sphinx_needs/config.py | 2 ++ sphinx_needs/need_item.py | 16 ++++++++++++--- sphinx_needs/needs.py | 1 + sphinx_needs/needs_schema.py | 37 ++++++++++++++++++++++++++++----- tests/test_need_item_api.py | 19 +++++++++++++++++ tests/test_needs_schema.py | 40 ++++++++++++++++++++++++++++++++++++ 7 files changed, 111 insertions(+), 8 deletions(-) diff --git a/docs/configuration.rst b/docs/configuration.rst index 3e9777ac7..27326635b 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -382,6 +382,8 @@ Each configured link can define: Default: ``False``. - ``parse_dynamic_functions``: If set to ``True``, the field will support :ref:`dynamic_functions`. Default: the value of :ref:`needs_parse_dynamic_functions` (``True``). +- ``parse_conditions``: If set to ``False``, the ``[condition]`` bracket syntax will not be parsed for this link type. + Default: ``True``. - ``incoming`` (optional): Incoming text, to use for incoming links. E.g. "is blocked by". Default: " incoming". - ``outgoing`` (optional): Outgoing text, to use for outgoing links. E.g. "blocks". Default: "". - ``copy`` (optional): True/False. If True, the links will be copied also to the common link-list (link type ``links``). @@ -2830,6 +2832,8 @@ Each configured link should define: Default: ``False``. - ``parse_dynamic_functions``: If set to ``True``, the field will support :ref:`dynamic_functions`. Default: the value of :ref:`needs_parse_dynamic_functions` (``True``). +- ``parse_conditions``: If set to ``False``, the ``[condition]`` bracket syntax will not be parsed for this link type. + Default: ``True``. - ``incoming`` (optional): Incoming text, to use for incoming links. E.g. "is blocked by". - ``outgoing`` (optional): Outgoing text, to use for outgoing links. E.g. "blocks". - ``copy`` (optional): True/False. If True, the links will be copied also to the common link-list (link type ``links``). diff --git a/sphinx_needs/config.py b/sphinx_needs/config.py index d213496ef..7f53b5783 100644 --- a/sphinx_needs/config.py +++ b/sphinx_needs/config.py @@ -304,6 +304,8 @@ class NeedLinksConfig(TypedDict, total=False): """Whether variants are parsed in this field.""" parse_dynamic_functions: NotRequired[bool] """Whether dynamic functions are parsed in this field.""" + parse_conditions: NotRequired[bool] + """Whether conditions (bracket syntax) are parsed in this field.""" class LinkOptionsType(NeedLinksConfig): diff --git a/sphinx_needs/need_item.py b/sphinx_needs/need_item.py index d1501bf84..7f0f7481f 100644 --- a/sphinx_needs/need_item.py +++ b/sphinx_needs/need_item.py @@ -247,7 +247,7 @@ class NeedLink: condition: str | None = None @staticmethod - def from_string(link_str: str) -> NeedLink: + def from_string(link_str: str, *, parse_conditions: bool = True) -> NeedLink: """Parse a link from a string (infallible, best-effort). Supports formats: ``ID``, ``ID.part``, ``ID[condition]``, @@ -256,20 +256,30 @@ def from_string(link_str: str) -> NeedLink: On malformed brackets (unclosed, trailing text), falls back to parsing without a condition. Use :meth:`from_string_with_warnings` if you need to detect malformed input. + + :param parse_conditions: Whether to parse ``[condition]`` brackets. """ - return NeedLink.from_string_with_warnings(link_str)[0] + return NeedLink.from_string_with_warnings( + link_str, parse_conditions=parse_conditions + )[0] @staticmethod - def from_string_with_warnings(link_str: str) -> tuple[NeedLink, list[str]]: + def from_string_with_warnings( + link_str: str, *, parse_conditions: bool = True + ) -> tuple[NeedLink, list[str]]: """Parse a link from a string, returning warnings for malformed input. Same parsing as :meth:`from_string`, but returns a list of warning messages instead of silently ignoring malformed brackets. + :param parse_conditions: Whether to parse ``[condition]`` brackets. :returns: A tuple of ``(NeedLink, warnings)``. """ warnings: list[str] = [] + if not parse_conditions: + return NeedLink.parse_address(link_str), warnings + # Find the first '[' that could start a condition bracket_start = link_str.find("[") if bracket_start <= 0: diff --git a/sphinx_needs/needs.py b/sphinx_needs/needs.py index ad4097cef..c8aba344d 100644 --- a/sphinx_needs/needs.py +++ b/sphinx_needs/needs.py @@ -1032,6 +1032,7 @@ def create_schema(app: Sphinx, env: BuildEnvironment, _docnames: list[str]) -> N "parse_dynamic_functions", needs_config._parse_dynamic_functions ), parse_variants=link.get("parse_variants", False), + parse_conditions=link.get("parse_conditions", True), directive_option=True, display=display_config, copy=link.get("copy", False), diff --git a/sphinx_needs/needs_schema.py b/sphinx_needs/needs_schema.py index f87daa3e1..8fbde48e3 100644 --- a/sphinx_needs/needs_schema.py +++ b/sphinx_needs/needs_schema.py @@ -549,6 +549,7 @@ class LinkSchema: allow_extend: bool = False parse_dynamic_functions: bool = False parse_variants: bool = False + parse_conditions: bool = True allow_defaults: bool = False predicate_defaults: tuple[ tuple[str, LinksLiteralValue | LinksFunctionArray], @@ -589,6 +590,8 @@ def __post_init__(self) -> None: raise ValueError("parse_dynamic_functions must be a boolean.") if not isinstance(self.parse_variants, bool): raise ValueError("parse_variants must be a boolean.") + if not isinstance(self.parse_conditions, bool): + raise ValueError("parse_conditions must be a boolean.") if not isinstance(self.allow_defaults, bool): raise ValueError("allow_defaults must be a boolean.") if not isinstance(self.allow_extend, bool): @@ -725,7 +728,10 @@ def convert_directive_option( has_df_or_vf = False array: list[NeedLink | DynamicFunctionParsed | VariantFunctionParsed] = [] for item in _split_link_list( - value, self.parse_dynamic_functions, self.parse_variants + value, + self.parse_dynamic_functions, + self.parse_variants, + parse_conditions=self.parse_conditions, ): if isinstance(item, LinkSplitWarning): # TODO bubble up as warning? @@ -789,13 +795,29 @@ def convert_or_type_check( VariantFunctionParsed.from_string(item.strip()[2:-2]) ) else: - new_value.append(NeedLink.from_string(item)) + new_value.append( + NeedLink.from_string( + item, parse_conditions=self.parse_conditions + ) + ) if has_function: return LinksFunctionArray(tuple(new_value)) else: - return LinksLiteralValue([NeedLink.from_string(v) for v in value]) + return LinksLiteralValue( + [ + NeedLink.from_string( + v, parse_conditions=self.parse_conditions + ) + for v in value + ] + ) else: - return LinksLiteralValue([NeedLink.from_string(v) for v in value]) + return LinksLiteralValue( + [ + NeedLink.from_string(v, parse_conditions=self.parse_conditions) + for v in value + ] + ) class FieldsSchema: @@ -1007,6 +1029,8 @@ def _split_link_list( text: str, parse_dynamic_functions: bool, parse_variants: bool, + *, + parse_conditions: bool = True, ) -> Iterator[ NeedLink | DynamicFunctionParsed | VariantFunctionParsed | LinkSplitWarning ]: @@ -1030,6 +1054,7 @@ def _split_link_list( :param text: The string to split. :param parse_dynamic_functions: Whether to parse ``[[...]]`` dynamic functions. :param parse_variants: Whether to parse ``<<...>>`` variant functions. + :param parse_conditions: Whether to parse ``[condition]`` brackets. :yields: Parsed link items, or ``LinkSplitWarning`` for non-fatal issues (e.g. text adjacent to a dynamic/variant function, unclosed brackets). """ @@ -1045,7 +1070,9 @@ def _flush_current() -> NeedLink | LinkSplitWarning | None: _current = "" if not stripped: return None - link, warnings = NeedLink.from_string_with_warnings(stripped) + link, warnings = NeedLink.from_string_with_warnings( + stripped, parse_conditions=parse_conditions + ) if warnings: return LinkSplitWarning(warnings[0]) return link diff --git a/tests/test_need_item_api.py b/tests/test_need_item_api.py index f6b3b78e2..04ff2e6ef 100644 --- a/tests/test_need_item_api.py +++ b/tests/test_need_item_api.py @@ -496,6 +496,25 @@ def test_warnings_trailing(self) -> None: assert len(warnings) == 1 assert "Unexpected text" in warnings[0] + def test_parse_conditions_false_with_brackets(self) -> None: + link = NeedLink.from_string("NEED-1[cond]", parse_conditions=False) + assert link == NeedLink(id="NEED-1[cond]", condition=None) + + def test_parse_conditions_false_with_part_and_brackets(self) -> None: + link = NeedLink.from_string("NEED-1.partA[cond]", parse_conditions=False) + assert link == NeedLink(id="NEED-1", part="partA[cond]", condition=None) + + def test_parse_conditions_false_plain_id(self) -> None: + link = NeedLink.from_string("NEED-1", parse_conditions=False) + assert link == NeedLink(id="NEED-1") + + def test_parse_conditions_false_with_warnings(self) -> None: + link, warnings = NeedLink.from_string_with_warnings( + "NEED-1[cond]", parse_conditions=False + ) + assert link == NeedLink(id="NEED-1[cond]", condition=None) + assert warnings == [] + class TestNeedLinkToLinkString: """Tests for NeedLink.to_link_string.""" diff --git a/tests/test_needs_schema.py b/tests/test_needs_schema.py index 16d29c71c..959706959 100644 --- a/tests/test_needs_schema.py +++ b/tests/test_needs_schema.py @@ -991,6 +991,46 @@ def test_split_link_list(text, parse_df, parse_vf, expected): assert list(_split_link_list(text, parse_df, parse_vf)) == expected +@pytest.mark.parametrize( + "text, parse_df, parse_vf, expected", + [ + # Brackets treated as literal text when parse_conditions=False + ( + "REQ[cond]", + True, + True, + [NeedLink(id="REQ[cond]")], + ), + # Part with brackets treated as literal text + ( + "REQ.partA[cond]", + True, + True, + [NeedLink(id="REQ", part="partA[cond]")], + ), + # Plain ID unchanged + ( + "REQ-001", + True, + True, + [NeedLink(id="REQ-001")], + ), + # Multiple items with brackets as literal text + ( + "REQ[c1];SPEC[c2]", + True, + True, + [NeedLink(id="REQ[c1]"), NeedLink(id="SPEC[c2]")], + ), + ], +) +def test_split_link_list_no_parse_conditions(text, parse_df, parse_vf, expected): + assert ( + list(_split_link_list(text, parse_df, parse_vf, parse_conditions=False)) + == expected + ) + + @pytest.mark.parametrize( "parent_schema,child_schema,expected", [ From 5bfa2fee784d5616c15a81a786e73865ec80edbd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 21:05:31 +0000 Subject: [PATCH 3/4] Add end-to-end test for parse_conditions config in doc_link_conditions Co-authored-by: chrisjsewell <2997570+chrisjsewell@users.noreply.github.com> --- tests/__snapshots__/test_link_conditions.ambr | 90 ++++++++++++++++++- tests/doc_test/doc_link_conditions/conf.py | 8 ++ tests/doc_test/doc_link_conditions/index.rst | 10 +++ tests/test_link_conditions.py | 32 +++++++ 4 files changed, 138 insertions(+), 2 deletions(-) diff --git a/tests/__snapshots__/test_link_conditions.ambr b/tests/__snapshots__/test_link_conditions.ambr index 1f71caed4..dede524b7 100644 --- a/tests/__snapshots__/test_link_conditions.ambr +++ b/tests/__snapshots__/test_link_conditions.ambr @@ -232,8 +232,31 @@ 'type': 'spec', 'type_name': 'Specification', }), + 'SPEC_RAW_001': dict({ + 'content': ''' + The ``raw_links`` link type has ``parse_conditions: False``, + so the brackets are treated as literal ID text, not as a condition. + ''', + 'docname': 'index', + 'external_css': 'external_link', + 'has_dead_links': True, + 'has_forbidden_dead_links': True, + 'id': 'SPEC_RAW_001', + 'lineno': 66, + 'raw_links': list([ + 'REQ_001[status=="open"]', + ]), + 'section_name': 'Parse Conditions Disabled', + 'sections': list([ + 'Parse Conditions Disabled', + 'LINK CONDITIONS TEST', + ]), + 'title': 'Spec with raw link containing brackets', + 'type': 'spec', + 'type_name': 'Specification', + }), }), - 'needs_amount': 12, + 'needs_amount': 13, 'needs_defaults_removed': True, 'needs_schema': dict({ '$schema': 'http://json-schema.org/draft-07/schema#', @@ -584,6 +607,26 @@ 'null', ]), }), + 'raw_links': dict({ + 'default': list([ + ]), + 'description': 'Link field', + 'field_type': 'links', + 'items': dict({ + 'type': 'string', + }), + 'type': 'array', + }), + 'raw_links_back': dict({ + 'default': list([ + ]), + 'description': 'Backlink field', + 'field_type': 'backlinks', + 'items': dict({ + 'type': 'string', + }), + 'type': 'array', + }), 'section_name': dict({ 'default': None, 'description': 'Simply the first section.', @@ -960,8 +1003,31 @@ 'type': 'spec', 'type_name': 'Specification', }), + 'SPEC_RAW_001': dict({ + 'content': ''' + The ``raw_links`` link type has ``parse_conditions: False``, + so the brackets are treated as literal ID text, not as a condition. + ''', + 'docname': 'index', + 'external_css': 'external_link', + 'has_dead_links': True, + 'has_forbidden_dead_links': True, + 'id': 'SPEC_RAW_001', + 'lineno': 66, + 'raw_links': list([ + 'REQ_001[status=="open"]', + ]), + 'section_name': 'Parse Conditions Disabled', + 'sections': list([ + 'Parse Conditions Disabled', + 'LINK CONDITIONS TEST', + ]), + 'title': 'Spec with raw link containing brackets', + 'type': 'spec', + 'type_name': 'Specification', + }), }), - 'needs_amount': 12, + 'needs_amount': 13, 'needs_defaults_removed': True, 'needs_schema': dict({ '$schema': 'http://json-schema.org/draft-07/schema#', @@ -1312,6 +1378,26 @@ 'null', ]), }), + 'raw_links': dict({ + 'default': list([ + ]), + 'description': 'Link field', + 'field_type': 'links', + 'items': dict({ + 'type': 'string', + }), + 'type': 'array', + }), + 'raw_links_back': dict({ + 'default': list([ + ]), + 'description': 'Backlink field', + 'field_type': 'backlinks', + 'items': dict({ + 'type': 'string', + }), + 'type': 'array', + }), 'section_name': dict({ 'default': None, 'description': 'Simply the first section.', diff --git a/tests/doc_test/doc_link_conditions/conf.py b/tests/doc_test/doc_link_conditions/conf.py index a3d6b68f7..045278048 100644 --- a/tests/doc_test/doc_link_conditions/conf.py +++ b/tests/doc_test/doc_link_conditions/conf.py @@ -11,6 +11,14 @@ }, ] +needs_links = { + "raw_links": { + "outgoing": "raw links outgoing", + "incoming": "raw links incoming", + "parse_conditions": False, + }, +} + needs_types = [ { "directive": "req", diff --git a/tests/doc_test/doc_link_conditions/index.rst b/tests/doc_test/doc_link_conditions/index.rst index 05f72e65e..edfb9e78a 100644 --- a/tests/doc_test/doc_link_conditions/index.rst +++ b/tests/doc_test/doc_link_conditions/index.rst @@ -59,3 +59,13 @@ Imported Needs -------------- .. needimport:: needs_test_conditions.json + +Parse Conditions Disabled +------------------------- + +.. spec:: Spec with raw link containing brackets + :id: SPEC_RAW_001 + :raw_links: REQ_001[status=="open"] + + The ``raw_links`` link type has ``parse_conditions: False``, + so the brackets are treated as literal ID text, not as a condition. diff --git a/tests/test_link_conditions.py b/tests/test_link_conditions.py index 5fc9b0400..9993155a4 100644 --- a/tests/test_link_conditions.py +++ b/tests/test_link_conditions.py @@ -41,6 +41,9 @@ def test_link_conditions(test_app: Sphinx, snapshot): "srcdir/index.rst:52: WARNING: Need 'SPEC_006' link 'REQ_001' in field 'links': condition '\"open\" in tags' not satisfied by target need 'REQ_001' [needs.link_condition_failed]", # IMP_COND_FAIL imported via needimport, links to REQ_002[status=="open"] which fails "srcdir/index.rst:61: WARNING: Need 'IMP_COND_FAIL' link 'REQ_002' in field 'links': condition 'status==\"open\"' not satisfied by target need 'REQ_002' [needs.link_condition_failed]", + # SPEC_RAW_001 uses raw_links (parse_conditions=False), so brackets are literal ID text. + # The link target 'REQ_001[status=="open"]' doesn't exist as a need, so it's a dead link. + "srcdir/index.rst:66: WARNING: Need 'SPEC_RAW_001' has unknown outgoing link 'REQ_001[status==\"open\"]' in field 'raw_links' [needs.link_outgoing]", ] needs_data = json.loads(Path(app.outdir, "needs.json").read_text()) @@ -66,3 +69,32 @@ def test_json_excludes_link_conditions_when_disabled(test_app: Sphinx, snapshot) needs_data = json.loads(Path(app.outdir, "needs.json").read_text()) assert needs_data == snapshot(exclude=props("created", "project", "creator")) + + +@pytest.mark.parametrize( + "test_app", + [ + { + "buildername": "html", + "srcdir": "doc_test/doc_link_conditions", + "no_plantuml": True, + } + ], + indirect=True, +) +def test_parse_conditions_disabled(test_app: Sphinx): + """Test that parse_conditions=False treats brackets as literal ID text.""" + app = test_app + app.build() + + needs_data = json.loads(Path(app.outdir, "needs.json").read_text()) + spec_raw = needs_data["versions"][""]["needs"]["SPEC_RAW_001"] + + # With parse_conditions=False, the brackets are NOT parsed as a condition. + # The entire string 'REQ_001[status=="open"]' is treated as a literal link ID. + assert spec_raw["raw_links"] == ['REQ_001[status=="open"]'] + + # Verify no condition was extracted (the link has no condition field) + # The raw_links_back on REQ_001 should be empty since the literal ID doesn't match + req_001 = needs_data["versions"][""]["needs"]["REQ_001"] + assert req_001.get("raw_links_back", []) == [] From 4c19a5088b0b5aed66b8b6efdfa4c6364113752a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 22:15:56 +0000 Subject: [PATCH 4/4] Update test_basic_doc snapshot to include parse_conditions field in LinkSchema Co-authored-by: chrisjsewell <2997570+chrisjsewell@users.noreply.github.com> --- tests/__snapshots__/test_basic_doc.ambr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/__snapshots__/test_basic_doc.ambr b/tests/__snapshots__/test_basic_doc.ambr index eecf64473..56192258e 100644 --- a/tests/__snapshots__/test_basic_doc.ambr +++ b/tests/__snapshots__/test_basic_doc.ambr @@ -651,11 +651,11 @@ 'hide': FieldSchema(name='hide', description='If true, the need is not rendered.', schema={'type': 'boolean'}, nullable=False, directive_option=True, parse_dynamic_functions=True, parse_variants=False, allow_defaults=True, allow_extend=True, predicate_defaults=(), default=FieldLiteralValue(value=False)), 'id_prefix': FieldSchema(name='id_prefix', description='Added by service github-issues', schema={'type': 'string'}, nullable=True, directive_option=True, parse_dynamic_functions=True, parse_variants=False, allow_defaults=True, allow_extend=True, predicate_defaults=(), default=None), 'layout': FieldSchema(name='layout', description='Key of the layout, which is used to render the need.', schema={'type': 'string'}, nullable=True, directive_option=True, parse_dynamic_functions=True, parse_variants=False, allow_defaults=True, allow_extend=True, predicate_defaults=(), default=None), - 'links': LinkSchema(name='links', description='Link field', schema={'type': 'array', 'items': {'type': 'string'}}, directive_option=True, allow_extend=True, parse_dynamic_functions=True, parse_variants=False, allow_defaults=True, predicate_defaults=(), default=LinksLiteralValue(value=[]), display=LinkDisplayConfig(incoming='links incoming', outgoing='links outgoing', color='#000000', style='', style_part='dotted', style_start='-', style_end='->'), copy=False, allow_dead_links=False), + 'links': LinkSchema(name='links', description='Link field', schema={'type': 'array', 'items': {'type': 'string'}}, directive_option=True, allow_extend=True, parse_dynamic_functions=True, parse_variants=False, parse_conditions=True, allow_defaults=True, predicate_defaults=(), default=LinksLiteralValue(value=[]), display=LinkDisplayConfig(incoming='links incoming', outgoing='links outgoing', color='#000000', style='', style_part='dotted', style_start='-', style_end='->'), copy=False, allow_dead_links=False), 'max_amount': FieldSchema(name='max_amount', description='Added by service github-issues', schema={'type': 'string'}, nullable=True, directive_option=True, parse_dynamic_functions=True, parse_variants=False, allow_defaults=True, allow_extend=True, predicate_defaults=(), default=None), 'max_content_lines': FieldSchema(name='max_content_lines', description='Added by service github-issues', schema={'type': 'string'}, nullable=True, directive_option=True, parse_dynamic_functions=True, parse_variants=False, allow_defaults=True, allow_extend=True, predicate_defaults=(), default=None), 'params': FieldSchema(name='params', description='Added by service open-needs', schema={'type': 'string'}, nullable=True, directive_option=True, parse_dynamic_functions=True, parse_variants=False, allow_defaults=True, allow_extend=True, predicate_defaults=(), default=None), - 'parent_needs': LinkSchema(name='parent_needs', description='Link field', schema={'type': 'array', 'items': {'type': 'string'}}, directive_option=True, allow_extend=True, parse_dynamic_functions=True, parse_variants=False, allow_defaults=True, predicate_defaults=(), default=LinksLiteralValue(value=[]), display=LinkDisplayConfig(incoming='child needs', outgoing='parent needs', color='#333333', style='', style_part='dotted', style_start='-', style_end='->'), copy=False, allow_dead_links=False), + 'parent_needs': LinkSchema(name='parent_needs', description='Link field', schema={'type': 'array', 'items': {'type': 'string'}}, directive_option=True, allow_extend=True, parse_dynamic_functions=True, parse_variants=False, parse_conditions=True, allow_defaults=True, predicate_defaults=(), default=LinksLiteralValue(value=[]), display=LinkDisplayConfig(incoming='child needs', outgoing='parent needs', color='#333333', style='', style_part='dotted', style_start='-', style_end='->'), copy=False, allow_dead_links=False), 'post_template': FieldSchema(name='post_template', description='The template key, if the post_content was created from a jinja template.', schema={'type': 'string'}, nullable=True, directive_option=True, parse_dynamic_functions=False, parse_variants=False, allow_defaults=True, allow_extend=False, predicate_defaults=(), default=None), 'pre_template': FieldSchema(name='pre_template', description='The template key, if the pre_content was created from a jinja template.', schema={'type': 'string'}, nullable=True, directive_option=True, parse_dynamic_functions=False, parse_variants=False, allow_defaults=True, allow_extend=False, predicate_defaults=(), default=None), 'prefix': FieldSchema(name='prefix', description='Added by service open-needs', schema={'type': 'string'}, nullable=True, directive_option=True, parse_dynamic_functions=True, parse_variants=False, allow_defaults=True, allow_extend=True, predicate_defaults=(), default=None),