Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion sphinx_needs/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ class ExtraOptionParams:

description: str
"""A description of the option."""
dynamic_functions: bool | None
"""If True, allow dynamic functions when defining this field."""
variant_functions: bool | None
"""If True, allow variant functions when defining this field."""
schema: (
ExtraOptionStringSchemaType
| ExtraOptionBooleanSchemaType
Expand Down Expand Up @@ -106,6 +110,8 @@ def add_extra_option(
name: str,
description: str,
*,
dynamic_functions: bool | None = None,
variant_functions: bool | None = None,
schema: ExtraOptionStringSchemaType
| ExtraOptionBooleanSchemaType
| ExtraOptionIntegerSchemaType
Expand Down Expand Up @@ -139,9 +145,19 @@ def add_extra_option(
)

raise NeedsApiConfigWarning(f"Option {name} already registered.")
if not isinstance(dynamic_functions, bool | None):
raise NeedsApiConfigWarning(
f"dynamic_functions for {name!r} must be a bool or None"
)
if not isinstance(variant_functions, bool | None):
raise NeedsApiConfigWarning(
f"variant_functions for {name!r} must be a bool or None"
)
self._extra_options[name] = ExtraOptionParams(
description=description,
schema=schema,
dynamic_functions=dynamic_functions,
variant_functions=variant_functions,
)

@property
Expand Down Expand Up @@ -270,6 +286,10 @@ class LinkOptionsType(TypedDict, total=False):
The schema is applied locally on unresolved links, i.e. on the list of string ids.
For more granular control and graph traversal, use the `needs_schema_definitions` configuration.
"""
dynamic_functions: NotRequired[bool]
"""If True, allow dynamic functions when defining this field (default: True)."""
variant_functions: NotRequired[bool]
"""If True, allow variant functions when defining this field (default: False)."""


class NeedType(TypedDict):
Expand Down Expand Up @@ -300,6 +320,19 @@ class NeedExtraOption(TypedDict):
If given, the schema will apply to all needs that use this option.
For more granular control, use the `needs_schema_definitions` configuration.
"""
dynamic_functions: NotRequired[bool]
"""If True, allow dynamic functions when defining this field (default: True)."""
variant_functions: NotRequired[bool]
"""If True, allow variant functions when defining this field (default: False)."""


class NeedCoreOption(TypedDict):
"""Defines an override for a core option for needs"""

dynamic_functions: NotRequired[bool]
"""If True, allow dynamic functions when defining this field (default: True)."""
variant_functions: NotRequired[bool]
"""If True, allow variant functions when defining this field (default: False)."""


class NeedStatusesOption(TypedDict):
Expand Down Expand Up @@ -542,10 +575,14 @@ def get_default(cls, name: str) -> Any:
default=30, metadata={"rebuild": "html", "types": (int,)}
)
"""Maximum length of the title in the need role output."""
core_options: Mapping[str, NeedCoreOption] = field(
default_factory=dict, metadata={"rebuild": "html", "types": (dict,)}
)
"""Override certain core fields for needs."""
_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."""
"""List of extra fields for needs, that get added as directive options and need fields."""

@property
def extra_options(self) -> Mapping[str, ExtraOptionParams]:
Expand Down
100 changes: 73 additions & 27 deletions sphinx_needs/needs.py
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,8 @@ def load_config(app: Sphinx, *_args: Any) -> None:
for option in needs_config._extra_options:
description = "Added by needs_extra_options config"
schema = None
dynamic_functions = None
variant_functions = None
if isinstance(option, str):
name = option
elif isinstance(option, dict):
Expand All @@ -510,6 +512,8 @@ def load_config(app: Sphinx, *_args: Any) -> None:
continue
description = option.get("description", description)
schema = option.get("schema")
dynamic_functions = option.get("dynamic_functions")
variant_functions = option.get("variant_functions")
else:
log_warning(
LOGGER,
Expand All @@ -519,7 +523,14 @@ def load_config(app: Sphinx, *_args: Any) -> None:
)
continue

_NEEDS_CONFIG.add_extra_option(name, description, schema=schema, override=True)
_NEEDS_CONFIG.add_extra_option(
name,
description,
schema=schema,
override=True,
dynamic_functions=dynamic_functions,
variant_functions=variant_functions,
)

# ensure options for `needgantt` functionality are added to the extra options
for option in (needs_config.duration_option, needs_config.completion_option):
Expand Down Expand Up @@ -712,6 +723,17 @@ def check_configuration(app: Sphinx, config: Config) -> None:
" Please use another name in your config (needs_extra_links)."
)

# check for correct keys in needs_core_options
allowed_core_options = {
key
for key, val in NeedsCoreFields.items()
if val.get("add_to_field_schema", False)
}
if disallowed_keys := set(needs_config.core_options) - allowed_core_options:
raise NeedsConfigException(
f"Keys in needs_core_options are not allowed: {disallowed_keys}"
)

# Check if option and link are using the same name
for link in link_types:
if link in extra_options:
Expand All @@ -725,29 +747,20 @@ def check_configuration(app: Sphinx, config: Config) -> None:
" This is not allowed.".format(link + "_back")
)

external_variants = needs_config.variants
external_variant_options = needs_config.variant_options
for value in external_variants.values():
for value in needs_config.variants.values():
# Check if external filter values is really a string
if not isinstance(value, str):
raise NeedsConfigException(
f"Variant filter value: {value} from needs_variants {external_variants} is not a string."
f"Variant filter value: {value} from needs_variants {needs_config.variants} is not a string."
)

allowed_internal_variants = {
k for k, v in NeedsCoreFields.items() if v.get("allow_variants")
}
for option in external_variant_options:
# Check variant option is added to an allowed field
if option in link_types:
raise NeedsConfigException(
f"Variant option `{option}` is a link type. This is not allowed."
)
if option not in extra_options and option not in allowed_internal_variants:
raise NeedsConfigException(
f"Variant option `{option}` is not added in extra options. "
"This is not allowed."
)
if needs_config.variant_options:
log_warning(
LOGGER,
'Config option "needs_variant_options" is deprecated. Please enable "variant_functions" in "needs_core_options" or "needs_extra_options" instead.',
"deprecated",
None,
)

validate_schemas_config(needs_config)

Expand All @@ -758,13 +771,37 @@ def create_schema(app: Sphinx, env: BuildEnvironment, _docnames: list[str]) -> N
for name, data in NeedsCoreFields.items():
if not data.get("add_to_field_schema", False):
continue
overrides = needs_config.core_options.get(name, {})
type_ = data["schema"]["type"]
nullable = False
if isinstance(type_, list):
assert type_[1] == "null", "Only nullable types supported as list"
type_ = type_[0]
nullable = True
default = data["schema"].get("default", None)

allow_variants = overrides.get("variant_functions", False)
if name in needs_config.variant_options:
allow_variants = True
if allow_variants and not data.get("allow_variants"):
log_warning(
LOGGER,
f"Core field {name!r} cannot be configured to allow variant functions, as it does not support them.",
"config",
None,
)
allow_variants = False

allow_df = overrides.get("dynamic_functions", data.get("allow_df", False))
if allow_df and not data.get("allow_df"):
log_warning(
LOGGER,
f"Core field {name!r} cannot be configured to allow dynamic functions, as it does not support them.",
"config",
None,
)
allow_df = False

field = FieldSchema(
name=name,
description=data["description"],
Expand All @@ -774,10 +811,8 @@ def create_schema(app: Sphinx, env: BuildEnvironment, _docnames: list[str]) -> N
default=None if default is None else FieldLiteralValue(default),
allow_defaults=data.get("allow_default", False),
allow_extend=data.get("allow_extend", False),
allow_dynamic_functions=data.get("allow_df", False),
allow_variant_functions=name in needs_config.variant_options
if data.get("allow_variants", False)
else False,
allow_dynamic_functions=allow_df,
allow_variant_functions=allow_variants,
directive_option=name != "title",
)
try:
Expand All @@ -798,6 +833,15 @@ def create_schema(app: Sphinx, env: BuildEnvironment, _docnames: list[str]) -> N
type = extra.schema.get("type", "string")
if type == "array":
item_type = extra.schema.get("items", {}).get("type", "string") # type: ignore[attr-defined]

allow_variants = (
extra.variant_functions
if extra.variant_functions is not None
else False
)
if name in needs_config.variant_options:
allow_variants = True

field = FieldSchema(
name=name,
description=extra.description,
Expand All @@ -810,8 +854,10 @@ def create_schema(app: Sphinx, env: BuildEnvironment, _docnames: list[str]) -> N
default=None if extra.schema is not None else FieldLiteralValue(""),
allow_defaults=True,
allow_extend=True,
allow_dynamic_functions=True,
allow_variant_functions=name in needs_config.variant_options,
allow_dynamic_functions=True
if extra.dynamic_functions is None
else extra.dynamic_functions,
allow_variant_functions=allow_variants,
directive_option=True,
)
schema.add_extra_field(field)
Expand All @@ -833,8 +879,8 @@ def create_schema(app: Sphinx, env: BuildEnvironment, _docnames: list[str]) -> N
default=LinksLiteralValue([]),
allow_defaults=True,
allow_extend=True,
allow_dynamic_functions=True,
allow_variant_functions=name in needs_config.variant_options,
allow_dynamic_functions=link.get("dynamic_functions", True),
allow_variant_functions=link.get("variant_functions", False),
directive_option=True,
)
schema.add_link_field(link_field)
Expand Down
115 changes: 115 additions & 0 deletions tests/__snapshots__/test_variants.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,121 @@
})
# ---
# name: test_variant_options_html[test_app0]
dict({
'current_version': '',
'versions': dict({
'': dict({
'needs': dict({
'CV_0002': dict({
'docname': 'index',
'external_css': 'external_link',
'id': 'CV_0002',
'lineno': 19,
'section_name': 'Variant Handling Test',
'sections': list([
'Variant Handling Test',
]),
'status': 'open',
'tags': list([
'commence',
'start',
'begin',
]),
'title': 'Custom Variant',
'type': 'spec',
'type_name': 'Specification',
'value': 'start',
}),
'SPEC_003': dict({
'docname': 'index',
'external_css': 'external_link',
'id': 'SPEC_003',
'lineno': 26,
'section_name': 'Variant Handling Test',
'sections': list([
'Variant Handling Test',
]),
'status': 'open',
'title': 'Variant Specification',
'type': 'spec',
'type_name': 'Specification',
}),
'SPEC_004': dict({
'docname': 'index',
'external_css': 'external_link',
'id': 'SPEC_004',
'lineno': 30,
'section_name': 'Variant Handling Test',
'sections': list([
'Variant Handling Test',
]),
'status': 'unknown',
'title': 'Unknown Variant',
'type': 'spec',
'type_name': 'Specification',
}),
'SP_38823': dict({
'docname': 'index',
'external_css': 'external_link',
'id': 'SP_38823',
'lineno': 4,
'section_name': 'Variant Handling Test',
'sections': list([
'Variant Handling Test',
]),
'status': 'progress',
'tags': list([
'school',
'extension',
'needs',
]),
'title': 'No ID',
'type': 'spec',
'type_name': 'Specification',
}),
'ST_001': dict({
'author': 'Daniel Woste',
'docname': 'index',
'external_css': 'external_link',
'id': 'ST_001',
'lineno': 13,
'links_back': list([
'VA_003',
]),
'section_name': 'Variant Handling Test',
'sections': list([
'Variant Handling Test',
]),
'status': 'close',
'title': 'Test story',
'type': 'story',
'type_name': 'User Story',
}),
'VA_003': dict({
'docname': 'index',
'external_css': 'external_link',
'id': 'VA_003',
'lineno': 8,
'links': list([
'ST_001',
]),
'section_name': 'Variant Handling Test',
'sections': list([
'Variant Handling Test',
]),
'status': 'tags_implemented',
'title': 'Tags Example',
'type': 'spec',
'type_name': 'Specification',
}),
}),
'needs_amount': 6,
'needs_defaults_removed': True,
}),
}),
})
# ---
# name: test_variant_options_html_old[test_app0]
dict({
'current_version': '',
'versions': dict({
Expand Down
Loading