Skip to content
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
4 changes: 2 additions & 2 deletions BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,12 @@ compile_pip_requirements(

alias(
name = "format.check",
actual = "//tools/format:format.check",
actual = "//third_party/format:format.check",
)

alias(
name = "format.fix",
actual = "//tools/format:format",
actual = "//third_party/format:format",
)

filegroup(
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ generated in the following situations:


### 2.0.4-dev
* [TRLC_RST] Add tool to convert TRLC Requirements to Sphinx RST Files

* [TRLC] Add support for Python 3.14.

Expand Down
17 changes: 16 additions & 1 deletion MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,22 @@ pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip")

use_repo(pip, "trlc_dependencies")

[
pip.parse(
hub_name = "trlc_sphinx_dependencies",
python_version = python_version,
requirements_lock = "//tools/sphinx:requirements.txt",
)
for python_version in [
"3.9",
"3.10",
"3.11",
"3.12",
]
]

use_repo(pip, "trlc_sphinx_dependencies")

# Dev-only pip hub: tools required for linting, static analysis, etc.
pip_dev = use_extension(
"@rules_python//python/extensions:pip.bzl",
Expand All @@ -69,7 +85,6 @@ use_repo(pip_dev, "trlc_dev_dependencies")

http_archive = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")


# ---------------------------------------------------------------------------
# CVC5 binary archives
# Keep in synch with CVC5_DEFAULT_VERSION in util/fetch_cvc5.py
Expand Down
1,219 changes: 1,053 additions & 166 deletions MODULE.bazel.lock

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ The Python implementation can be used for several purposes:
* [Requirements Coverage Report](https://bmw-software-engineering.github.io/trlc/tracing.html)
* [Code Coverage Report](https://bmw-software-engineering.github.io/trlc/htmlcov/index.html)

### Tools Available

* [TRLC_RST](tools/trlc_rst/README.md) Convert TRLC Requirements for Sphinx Build

## Dependencies

### Run-time
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pyvcg==1.0.9
cvc5>=1.3.2
bigtree
4 changes: 4 additions & 0 deletions requirements_lock.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
#
# bazel run //:requirements.update
#
bigtree==0.17.0 \
--hash=sha256:5aa096ac6fddad9db0d38c8710242351fffe97463a1b3f00acbd32f815d61d4d \
--hash=sha256:b906913b47b2462bfb1fa16f492ecd00c2a6bc1eb16a6e75092affec77c251f1
# via -r requirements.txt
cvc5==1.3.2 \
--hash=sha256:04174c5cd3c858c972792debc96436632635db2f8c4098806cc7aa51a8de2920 \
--hash=sha256:13759b6b50d46aa79e3a75e8979b8f36d5eb3fbce9430c5de5ee014ab202344a \
Expand Down
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion tools/lint/linters.bzl → third_party/lint/linters.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ load("@aspect_rules_lint//lint:pylint.bzl", "lint_pylint_aspect")
load("@aspect_rules_lint//lint:ty.bzl", "lint_ty_aspect")

pylint = lint_pylint_aspect(
binary = Label("//tools/lint:pylint"),
binary = Label("//third_party/lint:pylint"),
config = Label("//:pyproject.toml"),
)

Expand Down
8 changes: 8 additions & 0 deletions tools/sphinx/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
load("@rules_python//python:pip.bzl", "compile_pip_requirements")

# Run: bazel run //tools/sphinx:requirements.update
compile_pip_requirements(
name = "requirements",
src = "requirements.txt.in",
requirements_txt = "requirements.txt",
)
44 changes: 44 additions & 0 deletions tools/sphinx/extensions/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Sphinx Extensions

Custom Sphinx extensions used when building TRLC-based documentation.

## `trlc` — requirement domain

**Location:** `trlc/trlc.py`

A Sphinx domain that makes `.. requirement:definition::` blocks (as produced by
[`trlc_rst`](../../trlc_rst/README.md)) first-class Sphinx objects.

### Features

- **`.. requirement:definition:: <name>`** — registers a requirement with a
stable HTML anchor (`requirement-<name>`) so it can be linked to from
anywhere in the documentation.
- **`:requirement:upstream-ref:`** / **`:requirement:downstream-ref:`** — roles
for cross-referencing a requirement by its fully-qualified name
(e.g. `MyProject.REQ_001`). A warning is emitted if the target is not found
or is ambiguous.
- **Requirement Index** — an auto-generated index page listing every defined
requirement in alphabetical order, grouped by first letter.

### Bazel usage

```python
load("@trlc_sphinx_dependencies//:requirements.bzl", "requirement")

py_library(
name = "trlc",
srcs = ["trlc.py"],
imports = ["."],
deps = [requirement("sphinx")],
)
```

Add the library to the `deps` of your `sphinx_build` target and register the
extension in `conf.py`:

```python
extensions = [
"trlc",
]
```
9 changes: 9 additions & 0 deletions tools/sphinx/extensions/trlc/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
load("@trlc_sphinx_dependencies//:requirements.bzl", "requirement")

py_library(
name = "trlc",
srcs = ["trlc.py"],
imports = ["."],
visibility = ["//visibility:public"],
deps = [requirement("sphinx")],
)
149 changes: 149 additions & 0 deletions tools/sphinx/extensions/trlc/trlc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
from collections import defaultdict
from pathlib import PurePosixPath

from sphinx import addnodes
from sphinx.application import Sphinx
from sphinx.directives import ObjectDescription
from sphinx.domains import Domain, Index
from sphinx.roles import XRefRole
from sphinx.util import logging
from sphinx.util.nodes import make_refnode

logger = logging.getLogger(__name__)


def _pretty_location(docname: str, line: int | None = None) -> str:
"""Return a short, human-readable source location.

Bazel sandboxes produce very long absolute paths that are not clickable in
VS Code terminals. This helper strips all leading path components up to
(and including) ``_sources/`` so that only the workspace-relative document
path remains. A line number is appended when available.
"""
path = docname
marker = "_sources/"
idx = path.find(marker)
if idx != -1:
path = path[idx + len(marker) :]
# Ensure .rst suffix for clickability
if not path.endswith(".rst"):
path += ".rst"
if line is not None:
return f"{path}:{line}"
return path


class RequirementsDirective(ObjectDescription):
has_content = True
required_arguments = 1
option_spec = {}

def handle_signature(self, sig, signode):
signode += addnodes.desc_name(text=sig)
return sig

def add_target_and_index(self, name, sig, signode):
signode["ids"].append("requirement" + "-" + sig)
requirements = self.env.get_domain("requirement")
requirements.add_requirement(sig)


class RequirementsIndex(Index):
name = "requirement"
localname = "Requirement Index"
shortname = "Requirement"

def generate(self, docnames=None):
content = defaultdict(list)

# sort the list of requirements in alphabetical order
requirements = self.domain.get_objects()
requirements = sorted(requirements, key=lambda requirement: requirement[0])

# group by first letter for name
for _name, dispname, typ, docname, anchor, _priority in requirements:
safe_char = dispname[0] if dispname and dispname[0].isalpha() else "?"
heading = safe_char.upper()
content[heading].append(
(
dispname,
0,
docname,
anchor,
docname,
"",
typ,
)
)

content = sorted(content.items(), key=lambda t: t[0].lower())

return content, True


class RequirementsDomain(Domain):
name = "requirement"
roles = {
"upstream-ref": XRefRole(),
"downstream-ref": XRefRole(),
}
directives = {
"definition": RequirementsDirective,
}
indices = (RequirementsIndex,)
initial_data = {
"requirements": [],
}

def get_full_qualified_name(self, node):
return f"requirement.{node.arguments[0]}"

def get_objects(self):
yield from self.data["requirements"]

def resolve_xref(self, env, fromdocname, builder, typ, target, node, contnode):
match = [
(docname, anchor)
for name, sig, typ, docname, anchor, prio in self.get_objects()
if sig == target
]
if len(match) == 1:
todocname, targ = match[0]
return make_refnode(builder, fromdocname, todocname, targ, contnode, targ)
elif len(match) > 1:
loc = _pretty_location(fromdocname, node.line if node else None)
logger.warning(
"(%s): '%s' is ambiguous (%d targets found)",
loc,
target,
len(match),
)
else:
loc = _pretty_location(fromdocname, node.line if node else None)
logger.warning(
"(%s): '%s' not found",
loc,
target,
)
return None

def add_requirement(self, signature):
name = f"requirement.{signature}"
anchor = f"requirement-{signature}"

self.data["requirements"].append(
(name, signature, "Requirement", self.env.docname, anchor, 0)
)

def merge_domaindata(self, docnames, otherdata):
self.data["requirements"].extend(otherdata["requirements"])


def setup(app: Sphinx):
app.add_domain(RequirementsDomain)

return {
"version": "0.1",
"parallel_read_safe": True,
"parallel_write_safe": True,
}
Loading
Loading