+
Skip to content
Closed
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
32 changes: 32 additions & 0 deletions docs/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,11 @@ load("@rules_pkg//pkg:mappings.bzl", "pkg_files")
load("@rules_pkg//pkg:tar.bzl", "pkg_tar")
load("@rules_python//python:pip.bzl", "compile_pip_requirements")
load("@rules_python//sphinxdocs:sphinx.bzl", "sphinx_build_binary", "sphinx_docs")
load("//tools/feature_flags:feature_flags.bzl", "define_feature_flags")
load("//tools/testing/pytest:defs.bzl", "score_py_pytest")

define_feature_flags(name = "filter_tags")

sphinx_requirements = all_requirements + [
"@rules_python//python/runfiles",
":plantuml_for_python",
Expand All @@ -67,6 +70,7 @@ sphinx_docs(
config = ":conf.py",
extra_opts = [
"--keep-going",
"-Dfilter_tags_file_path=$(location :filter_tags)",
],
formats = [
"html",
Expand All @@ -76,6 +80,7 @@ sphinx_docs(
"manual",
],
tools = [
":filter_tags",
":plantuml",
],
)
Expand Down Expand Up @@ -120,6 +125,7 @@ pkg_tar(
py_binary(
name = "incremental",
srcs = ["_tooling/incremental.py"],
data = [":flags_file"],
deps = sphinx_requirements,
)

Expand Down Expand Up @@ -171,3 +177,29 @@ score_py_pytest(
visibility = ["//visibility:public"],
deps = [":score_metamodel"],
)

py_library(
name = "score_feature_flag_handling",
srcs = glob(["_tooling/extensions/score_feature_flag_handling/**/*.py"]),
imports = ["_tooling/extensions"],
visibility = ["//visibility:public"],
)

score_py_pytest(
name = "score_feature_flag_handling_test",
size = "small",
srcs = glob(["_tooling/extensions/score_feature_flag_handling/tests/**/*.py"]),
args = [
"-s",
"-v",
],
visibility = ["//visibility:public"],
deps = [":score_feature_flag_handling"],
)

filegroup(
name = "flags_file",
srcs = [":filter_tags"],
output_group = "flags_file",
visibility = ["//visibility:public"],
)
130 changes: 130 additions & 0 deletions docs/_tooling/extensions/score_feature_flag_handling/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@

# Modularity Sphinx Extension

A Sphinx extension that enables conditional documentation rendering based on feature flags.

## Overview

The extension consists of two main components:

1. `feature_flags.bzl`: A Bazel translation layer that converts commands into tags
2. `score_feature_flag_handling`: A Sphinx extension that filters documentation based on feature flags

The list of tags to filter is built in 'reverse' from the features enabled. In an empty configuration all tags will be added to the 'disable' list. By adding a feature you remove the corresponding tags from the list, therefore enabling requirements with those tags.

**The extension is set up so it only disables requirements where *all* tags are contained within the filtered ones.**


## Usage

### Command Line

Configure features when building documentation:

```bash
docs build //docs:docs --//docs:feature1=true
```

## Configuration

### Feature Flags (feature_flags.bzl)

The `feature_flags.bzl` file takes care of the following things:

- Feature-to-tag translation
- Feature name to flag mappings
- Default values for flags
- Temporary file generation for flag storage


Example configuration:

Mapping of features to tags is defined as follows:

```bzl
FEATURE_TAG_MAPPING = {
"feature1": ["some-ip", "tag2"], # --//docs:feature1=true -> will display requirements with those tags
"second-feature": ["test-feat", "tag6"],
}
```

```bzl
def define_feature_flags(name):
bool_flag(
name = "feature1",
build_setting_default = False,
)
bool_flag(
name = "second-feature",
build_setting_default = False,
)

feature_flag_translator(
name = name,
flags = {":feature1": "True", ":second-feature": "True"},
)
```
As can be seen here, each flag needs to be registered as well as have a default value defined.
After adding new flags they can be added to the build command like so: `--//docs:<flag-name>=<value>`

If a feature flag is enabled requirements with corresponding tags are now **enabled**.


### Sphinx Extension (score_feature_flag_handling)

The extension processes the temporary flag file and disables documentation sections tagged with disabled features.

#### Use in requirements

All requirements which tags are **all** contained within the tags that we look for, will be disabled.
Here an example to illustrate the point.
We will disable the `test-feat` tag via feature flags. If we take the following example rst:
```rst
.. tool_req:: Test_TOOL
:id: TEST_TOOL_REQ
:tags: feature1, test-feat
:satisfies: TEST_STKH_REQ_1, TEST_STKH_REQ_20

We will see that this should still be rendered but 'TEST_STKH_REQ_1' will be missing from the 'satisfies' option

.. stkh_req:: Test_REQ disable
:id: TEST_STKH_REQ_1
:tags: test-feat

This is a requirement that we would want to disable via the feature flag

.. stkh_req:: Test_REQ do not disable
:id: TEST_STKH_REQ_20
:tags: feature1

This requirement will not be disabled.
```
We can then build it with our feature flag enabled via `bazel build //docs:docs --//docs:second-feature=true`
This will expand via our translation layer `feature_flag.bzl` into the tags `test-feat` and `tag5`.

**The extension is set up so it only disables requirements where *all* tags are contained within the filtered ones.**
In the rst above `TEST_TOOL_REQ` has the `test-feat` tag but it also has the `feature1` tag, therefore it won't be disabled.
In contrast, TEST_STKH_REQ_20 has only the `test-feat` tag, therefore it will be disabled and removed from any links as well.

If we now look at the rendered HTML:
![](rendered_html.png)

We can confirm that the `TEST_STKH_REQ_1` requirement is gone and so is the reference to it from `TEST_TOOL_REQ`.


*However, keep in mind that in the source code the actual underlying RST has not changed, it's just the HTML.*
This means that one can still see all the requirements there and also if searching for it will still find the document where it was mentioned.


### How the extension achieves this?

The extension uses a sphinx-needs built-in option called `hide`. If a need has the `hide=True` it will not be shown in the final HTML.
It also gathers a list of all 'hidden' requirements as it then in a second iteration removes these from any of the possible links.

#### Special cases

Needpie, Needtable etc. are special cases as these are not requirements. There are two wrappers inside `filter_overwrite.py` that add the general functionality of hiding all requirements that have `hide==True`. Therefore enabling normal use, without special restrictions needed to be adhered to.

### Decision Record

Please see [Decision Record](/docs/_tooling/extensions/modularity/decision_record.md) for more information.
115 changes: 115 additions & 0 deletions docs/_tooling/extensions/score_feature_flag_handling/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# *******************************************************************************
# Copyright (c) 2025 Contributors to the Eclipse Foundation
#
# See the NOTICE file(s) distributed with this work for additional
# information regarding copyright ownership.
#
# This program and the accompanying materials are made available under the
# terms of the Apache License Version 2.0 which is available at
# https://www.apache.org/licenses/LICENSE-2.0
#
# SPDX-License-Identifier: Apache-2.0
# *******************************************************************************
from pprint import pprint
from sphinx.application import Sphinx
from sphinx_needs.data import SphinxNeedsData
from sphinx_needs.config import NeedsSphinxConfig
from sphinx.environment import BuildEnvironment
from sphinx.util import logging
from .filter_overwrite import wrap_filter_common
import os
import json


logger = logging.getLogger(__name__)


def read_filter_tags(app: Sphinx) -> list:
"""
Helper function to read in the 'filter_tags' provided by `feature_flag.bzl`.

Args:
app: The current running Sphinx-Build application

Returns:
- List of tags e.g. ['some-ip','another-tag']

Errors:
- If 'filter_tags_file_path' can't be found in the config
- If something goes wrong when reading the file
"""
# asserting our worldview
assert hasattr(
app.config, "filter_tags_file_path"
), "Config missing filter_tags_file_path, this is mandatory."
filter_file = app.config.filter_tags_file_path
logger.debug(f"Found the following filter_tags_file_path: {filter_file}")
try:
with open(filter_file, "r") as f:
content = f.read().strip()
filter_tags = [tag.strip() for tag in content.split(",")] if content else []
logger.debug(f"found: {len(filter_tags)} filter tag.")
logger.debug(f"filter tags found: {filter_tags}")
return filter_tags
except Exception as e:
logger.error(f"could not read file: {filter_file}. Error: {e}")
raise e


def hide_needs(app: Sphinx, env: BuildEnvironment) -> None:
"""
Function that hides needs (requirements) if *all* of their tags are in the specified tags to hide.
Also deletes any references to hidden requirements, e.g. in 'satisfies' option.

Args:
app: The current running Sphinx-Build application, this will be supplied automatically
env: The current running BuildEnvironment, this will be supplied automatically
"""
filter_tags = read_filter_tags(app)

# Early return if no work needs to be done
if not filter_tags:
logger.debug("filter_tags was empty, no work to be done.")
return
need_data = SphinxNeedsData(env)
needs = need_data.get_needs_mutable()
extra_links = [x["option"] for x in NeedsSphinxConfig(env.config).extra_links]
rm_needs = []
for need_id, need in needs.items():
if need["hide"]:
rm_needs.append(need_id)
if (
need["tags"]
and all(tag in filter_tags for tag in need["tags"])
and not need["hide"]
):
rm_needs.append(need_id)
need["hide"] = True

needs_to_disable = set(rm_needs)

logger.debug(f"found {len(needs_to_disable)} requirements to be disabled.")
logger.debug(f"requirements found: {needs_to_disable}")

# Remove references
for need_id in needs:
for opt in extra_links:
needs[need_id][opt] = [
x for x in needs[need_id][opt] if x not in needs_to_disable
]
needs[need_id][opt + "_back"] = [
x for x in needs[need_id][opt + "_back"] if x not in needs_to_disable
]


def setup(app):
logger.debug("score_feature_flag_handling extension loaded")
app.add_config_value("filter_tags_file_path", None, "env")
app.connect("env-updated", hide_needs)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a specific event for this needs-before-sealing; func(app, needs), as mentioned in https://sphinx-needs.readthedocs.io/en/latest/changelog.html#improvements-to-filtering-at-scale

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm interesting, I must have made a mistake then. I tried this in the beginning but decided not go go with it as something didn't quite work. I will check it out again, as if we have such specific events it would be nicer to call them correctly.
Thanks for pointing it out.


wrap_filter_common()
return {
"version": "1.0",
"parallel_read_safe": True,
"parallel_write_safe": True,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Decision Record: How to disabale requirements

## Context
[Feature Flags](https://eclipse-score.github.io/score/process/guidance/feature_flags/index.html#feature-flags) require the functionality
of disable or enabeling requirements based on which feature flags were provided, in order to build relevant documentation only.
This decision record will contain what implementation was choosen to enable this feature.

## Decision
Use the 'hide' option from sphinx-needs objects to remove/disable requirements.

## Chosen Solution: 'hide' Option
The 'hide' option was selected as the primary implementation method.

### Advantages
- Can be set programmatically with minimal complexity
- Maintains document structure integrity
- Provides clear control over element visibility
- Predictable behavior in programmatic contexts

### Negatives
- Requirements still appear in the rst via 'view source code'
- Information only saved inside the `NeedsInfoType` objects

## Alternatives Considered

### Alternative 1: `:delete:` Option
**Why Not Chosen:**
- Lacks programmatic control capabilities. Can only be set hardcoded inside rst files

More info: [Delete option docs](https://sphinx-needs.readthedocs.io/en/latest/directives/need.html#delete)

### Alternative 2: `del_need` Method
**Why Not Chosen:**
- Introduces complexity in document node management
- Creates challenges in locating correct manipulation points
- Risk of document structure corruption during build
- Increases maintenance overhead due to complex node relationships

More info: [del_need docs](https://sphinx-needs.readthedocs.io/en/latest/api.html#sphinx_needs.api.need.del_need)

### Alternative 3: `variations` Approach
**Why Not Chosen:**
- Filtering behavior appears inconsistent or non functional when used programmatically
- Requires hardcoding in configuration files for reliable operation

More info: [variations docs](https://sphinx-needs.readthedocs.io/en/latest/configuration.html#needs-variants)

Loading
Loading
点击 这是indexloc提供的php浏览器服务,不要输入任何密码和下载