Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

♻️ Change needs_global_options format #1413

Merged
merged 2 commits into from
Mar 6, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
122 changes: 66 additions & 56 deletions docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -275,15 +275,54 @@ And use it like:
]

.. _needs_global_options:
.. _global_option_filters:

needs_global_options
~~~~~~~~~~~~~~~~~~~~
.. versionadded:: 0.3.0
.. versionchanged:: 5.1.0

Unknown keys are no longer accepted,
The format of the global options was change to be more explicit.

Unknown keys are also no longer accepted,
these should also be set in the :ref:`needs_extra_options` list.

.. dropdown:: Comparison to old format

.. code-block:: python
:caption: Old format

needs_global_options = {
"field1": "a",
"field2": ("a", 'status == "done"'),
"field3": ("a", 'status == "done"', "b"),
"field4": [
("a", 'status == "done"'),
("b", 'status == "ongoing"'),
("c", 'status == "other"', "d"),
],
}

.. code-block:: python
:caption: New format

needs_global_options = {
"field1": {"default": "a"},
"field2": {"predicates": [('status == "done"', "a")]},
"field3": {
"predicates": [('status == "done"', "a")],
"default": "b",
},
"field4": {
"predicates": [
('status == "done"', "a"),
('status == "ongoing"', "b"),
('status == "other"', "c"),
],
"default": "d",
},
}

This configuration allows for global defaults to be set for all needs,
for any of the following fields:

Expand All @@ -295,76 +334,47 @@ for any of the following fields:
- ``tags``
- ``constraints``

Defaults will be used if the field is not set specifically by the user and thus has a "empty" value.

.. code-block:: python

needs_extra_options = ["option1"]
needs_global_options = {
'option1': 'Fix value'
"tags": {"default": ["tag1", "tag2"]},
"option1": {"default": "new value"},
}

.. tip::

To set list based defaults, for links, tags and constraints, use a string based representation, e.g.

.. code-block:: python

needs_global_options = {
'tags': 'id1,id2,id3'
}

You can combine global options with :ref:`dynamic_functions` to automate data handling.
To set a default based on a one or more predicates, use the ``predicates`` key.
These predicates are a list of (:ref:`filter string <filter_string>`, value), evaluated in order, with the first match set as the default value.
If no predicates match, the ``default`` value is used (if present).

.. code-block:: python

needs_extra_options = ["option1"]
needs_global_options = {
'option1': '[[copy("id")]]'
"option1": {
# if field is unset:
"predicates": [
# if status is "done", set to "value1"
("status == 'done'", "value1"),
# else if status is "ongoing", set to "value2"
("status == 'ongoing'", "value2"),
]
# else, set to "value3"
"default": "value3",
}
}

.. _global_option_filters:

Predicate based defaults
++++++++++++++++++++++++
.. versionadded:: 0.4.3

Defaults can also be set based :ref:`filter_string` predicates.
This is useful when you want to set a default value based on the value of another field.

You can set the value of a global_option if only a given :ref:`filter_string` passes.
If the filter string does not pass, the option is not set or a given default value is set.

This can be provided in a few ways:

.. code-block:: python

needs_extra_options = ["field1"]
needs_global_options = {
# if field1 is unset and status is "done", set field1 to "a"
'field1': ('a', 'status == "done"')

.. code-block:: python

needs_extra_options = ["field1"]
needs_global_options = {
# if field1 is unset:
# if status is "done", set to "a",
# else, set to "b"
'field1': ('a', 'status == "done"', 'b')
.. tip::

You can combine global options with :ref:`dynamic_functions` to automate data handling.

.. code-block:: python
.. code-block:: python

needs_extra_options = ["field1"]
needs_global_options = {
# if field1 is unset:
# if status is "done", set to "a",
# else if status is "ongoing", set to "b",
# else if status is "other", set to "c",
# else, set to "d"
'field1': [
('a', 'status == "done"'),
('b', 'status == "ongoing"'),
('c', 'status == "other"', 'd')
]
needs_extra_options = ["option1"]
needs_global_options = {
"option1": {"default": '[[copy("id")]]'}
}

.. warning::

Expand Down
3 changes: 2 additions & 1 deletion docs/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ that can be used to add additional data to the item or further style its represe
For full options see the reference sections for :ref:`needs_types configuration <needs_types>` and :ref:`need items directive <need>`.

To add additional fields to the directive,
see the :ref:`needs_extra_options` and :ref:`needs_global_options`.
see :ref:`needs_extra_options`,
and to set default values see :ref:`needs_global_options`.

Enforcing valid need items
..........................
Expand Down
2 changes: 1 addition & 1 deletion sphinx_needs/api/need.py
Original file line number Diff line number Diff line change
Expand Up @@ -811,7 +811,7 @@ def _set_field_defaults(needs_info: NeedsInfoType, config: NeedsSphinxConfig) ->
for key, defaults in config.field_defaults.items():
if key not in needs_info or needs_info[key]:
continue
for predicate, v in defaults.get("predicate_defaults", []):
for predicate, v in defaults.get("predicates", []):
# use the first predicate that is satisfied
if filter_single_need(needs_info, config, predicate):
needs_info[key] = v
Expand Down
2 changes: 1 addition & 1 deletion sphinx_needs/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class ExtraOptionParams:
class FieldDefault(TypedDict):
"""Defines a default value for a field."""

predicate_defaults: NotRequired[list[tuple[str, Any]]]
predicates: NotRequired[list[tuple[str, Any]]]
"""List of (need filter, value) pairs for default predicate values.

Used if the field has not been specifically set.
Expand Down
72 changes: 56 additions & 16 deletions sphinx_needs/needs.py
Original file line number Diff line number Diff line change
Expand Up @@ -745,63 +745,94 @@
if "allow_default" in v
}
field_defaults: dict[str, FieldDefault] = {}
old_format = False
for key, value in needs_config._global_options.items():
single_default: FieldDefault = {}

if (
if isinstance(value, dict):
if unknown := set(value).difference({"predicates", "default"}):
log_warning(

Check warning on line 754 in sphinx_needs/needs.py

View check run for this annotation

Codecov / codecov/patch

sphinx_needs/needs.py#L754

Added line #L754 was not covered by tests
LOGGER,
f"needs_global_options {key!r} value contains unknown keys: {unknown}",
"config",
None,
)
single_default = { # type: ignore[assignment]
k: v for k, v in value.items() if k in {"predicates", "default"}
}
if "predicates" in single_default and (
not isinstance(single_default["predicates"], (list, tuple))
or not all(
isinstance(x, (list, tuple))
and len(x) == 2
and isinstance(x[0], str)
for x in single_default["predicates"]
)
):
log_warning(
LOGGER,
f"needs_global_options {key!r}, 'predicates', must be a list of (filter string, value) pairs",
"config",
None,
)
continue
elif (
isinstance(value, (list, tuple))
and len(value) > 0
and all(isinstance(x, (list, tuple)) for x in value)
):
single_default = {"predicate_defaults": []}
old_format = True
single_default = {"predicates": []}
last_idx = len(value) - 1
for sub_idx, sub_value in enumerate(value):
if len(sub_value) == 2:
# (value, predicate) pair
v, predicate = sub_value
single_default["predicate_defaults"].append((predicate, v))
single_default["predicates"].append((predicate, v))
elif len(sub_value) == 3:
# (value, predicate, default) triple
v, predicate, default = sub_value
single_default["predicate_defaults"].append((predicate, v))
single_default["predicates"].append((predicate, v))
if sub_idx == last_idx:
single_default["default"] = default
else:
log_warning(
LOGGER,
f"needs_global_options key {key!r}, item {sub_idx} has default value but is not the last item",
f"needs_global_options {key!r}, item {sub_idx}, has default value but is not the last item",
"config",
None,
)
else:
log_warning(
LOGGER,
f"needs_global_options key {key!r}, item {sub_idx} has an unknown format",
f"needs_global_options {key!r}, item {sub_idx}, has an unknown value format",
"config",
None,
)
elif isinstance(value, (list, tuple)):
old_format = True
if len(value) == 2:
# single (value, predicate) pair
v, predicate = value
single_default = {"predicate_defaults": [(predicate, v)]}
single_default = {"predicates": [(predicate, v)]}
elif len(value) == 3:
# single (value, predicate, default) triple
v, predicate, default = value
single_default = {
"predicate_defaults": [(predicate, v)],
"predicates": [(predicate, v)],
"default": default,
}
else:
log_warning(
LOGGER,
f"needs_global_options key {key!r} has an unknown format",
f"needs_global_options {key!r} has an unknown value format",
"config",
None,
)
continue
else:
# single value
old_format = True
single_default = {"default": value}

if key in needs_config.extra_options:
Expand All @@ -816,11 +847,20 @@
else:
log_warning(
LOGGER,
f"needs_global_options key {key!r} must also exist in needs_extra_options, needs_extra_links, or {sorted(allowed_internal_defaults)}",
f"needs_global_options {key!r} must also exist in needs_extra_options, needs_extra_links, or {sorted(allowed_internal_defaults)}",
"config",
None,
)

if old_format:
log_warning(
LOGGER,
"needs_global_options uses old, non-dict, format. "
f"please update to new format: {field_defaults}",
"deprecated",
None,
)

_NEEDS_CONFIG.field_defaults = field_defaults


Expand All @@ -836,7 +876,7 @@
if not isinstance(default["default"], type_):
log_warning(
LOGGER,
f"needs_global_options key {key!r} has a default value that is not of type {type_name!r}",
f"needs_global_options {key!r} has a default value that is not of type {type_name!r}",
"config",
None,
)
Expand All @@ -848,18 +888,18 @@
default["default"], None, " (in needs_global_options)"
)
]
if "predicate_defaults" in default:
for _, value in default["predicate_defaults"]:
if "predicates" in default:
for _, value in default["predicates"]:
if not isinstance(value, type_):
log_warning(
LOGGER,
f"needs_global_options key {key!r} has a predicate default value that is not of type {type_name!r}",
f"needs_global_options {key!r} has a predicate default value that is not of type {type_name!r}",
"config",
None,
)
return False
if type_name == "str_list":
default["predicate_defaults"] = [
default["predicates"] = [
(
predicate,
[
Expand All @@ -869,7 +909,7 @@
)
],
)
for predicate, value in default["predicate_defaults"]
for predicate, value in default["predicates"]
]
return True

Expand Down
Loading
Loading