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
227 changes: 22 additions & 205 deletions src/extensions/score_metamodel/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,29 @@
import pkgutil
import re
from collections.abc import Callable
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, cast
from typing import Any

from ruamel.yaml import YAML
from sphinx.application import Sphinx
from sphinx_needs import logging
from sphinx_needs.config import NeedType
from sphinx_needs.data import NeedsInfoType, NeedsView, SphinxNeedsData

from .external_needs import connect_external_needs
from .log import CheckLogger
from src.extensions.score_metamodel.external_needs import connect_external_needs
from src.extensions.score_metamodel.log import CheckLogger

# Import and re-export some types and functions for easier access
from src.extensions.score_metamodel.metamodel_types import (
ProhibitedWordCheck as ProhibitedWordCheck,
)
from src.extensions.score_metamodel.metamodel_types import (
ScoreNeedType as ScoreNeedType,
)
from src.extensions.score_metamodel.yaml_parser import (
default_options as default_options,
)
from src.extensions.score_metamodel.yaml_parser import (
load_metamodel_data as load_metamodel_data,
)

logger = logging.get_logger(__name__)

Expand All @@ -37,21 +48,6 @@
graph_checks: list[graph_check_function] = []


@dataclass
class ScoreNeedType(NeedType):
tags: list[str]
parts: int


@dataclass
class ProhibitedWordCheck:
name: str
option_check: dict[str, list[str]] = field(
default_factory=dict
) # { Option: [Forbidden words]}
types: list[str] = field(default_factory=list)


def parse_checks_filter(filter: str) -> list[str]:
"""
Parses a comma-separated list of check names.
Expand Down Expand Up @@ -239,185 +235,6 @@ def _check_external_optional_link_patterns(app: Sphinx, log: CheckLogger) -> Non
_validate_external_need_opt_links(need, opt_links, allowed_prefixes, log)


def convert_checks_to_dataclass(
checks_dict: dict[str, dict[str, Any]],
) -> list[ProhibitedWordCheck]:
return [
ProhibitedWordCheck(
name=check_name,
option_check={k: v for k, v in check_config.items() if k != "types"},
types=check_config.get("types", []),
)
for check_name, check_config in checks_dict.items()
]


def load_metamodel_data() -> dict[str, Any]:
"""
Load and process metamodel.yaml.

Returns:
dict: A dictionary with keys:
- 'needs_types': A list of processed need types.
- 'needs_extra_links': A list of extra link definitions.
- 'needs_extra_options': A sorted list of all option keys.
"""
yaml_path = Path(__file__).resolve().parent / "metamodel.yaml"

yaml = YAML()
with open(yaml_path, encoding="utf-8") as f:
data = cast(dict[str, Any], yaml.load(f))

# Access the custom validation block

types_dict = cast(dict[str, Any], data.get("needs_types", {}))
links_dict = cast(dict[str, Any], data.get("needs_extra_links", {}))
graph_check_dict = cast(dict[str, Any], data.get("graph_checks", {}))
global_base_options = cast(dict[str, Any], data.get("needs_types_base_options", {}))
global_base_options_optional_opts = cast(
dict[str, Any], global_base_options.get("optional_options", {})
)

# Get the stop_words and weak_words as separate lists
proh_checks_dict = cast(
dict[str, dict[str, Any]], data.get("prohibited_words_checks", {})
)
prohibited_words_checks = convert_checks_to_dataclass(proh_checks_dict)

# Default options by sphinx, sphinx-needs or anything else we need to account for
default_options_list = default_options()

# Convert "types" from {directive_name: {...}, ...} to a list of dicts
needs_types_list = []

all_options: set[str] = set()
for directive_name, directive_data in types_dict.items():
directive_name = cast(str, directive_name)
directive_data = cast(dict[str, Any], directive_data)
# Build up a single "needs_types" item
one_type: dict[str, Any] = {
"directive": directive_name,
"title": directive_data.get("title", ""),
"prefix": directive_data.get("prefix", ""),
}

if "color" in directive_data:
one_type["color"] = directive_data["color"]
if "style" in directive_data:
one_type["style"] = directive_data["style"]

# Store mandatory_options and optional_options directly as a dict
mandatory_options = cast(
dict[str, Any], directive_data.get("mandatory_options", {})
)
one_type["mandatory_options"] = mandatory_options
tags = cast(list[str], directive_data.get("tags", []))
one_type["tags"] = tags
parts = cast(int, directive_data.get("parts", 3))
one_type["parts"] = parts

optional_options = cast(
dict[str, Any], directive_data.get("optional_options", {})
)
optional_options.update(global_base_options_optional_opts)
one_type["opt_opt"] = optional_options

all_options.update(list(mandatory_options.keys()))
all_options.update(list(optional_options.keys()))

# mandatory_links => "req_link"
mand_links_yaml = cast(
dict[str, Any], directive_data.get("mandatory_links", {})
)
if mand_links_yaml:
one_type["req_link"] = [
(cast(str, k), cast(Any, v)) for k, v in mand_links_yaml.items()
]

# optional_links => "opt_link"
opt_links_yaml = cast(dict[str, Any], directive_data.get("optional_links", {}))
if opt_links_yaml:
one_type["opt_link"] = [
(cast(str, k), cast(Any, v)) for k, v in opt_links_yaml.items()
]

needs_types_list.append(one_type)

# Convert "links" dict -> list of {"option", "incoming", "outgoing"}
needs_extra_links_list: list[dict[str, str]] = []
for link_option, link_data in links_dict.items():
link_option = cast(str, link_option)
link_data = cast(dict[str, Any], link_data)
needs_extra_links_list.append(
{
"option": link_option,
"incoming": link_data.get("incoming", ""),
"outgoing": link_data.get("outgoing", ""),
}
)

# We have to remove all 'default options' from the extra options.
# As otherwise sphinx errors, due to an option being registered twice.
# They are still inside the extra options we extract to enable
# constraint checking via regex
needs_extra_options: list[str] = sorted(all_options - set(default_options_list))

return {
"prohibited_words_checks": prohibited_words_checks,
# "weak_words": weak_words_list,
"needs_types": needs_types_list,
"needs_extra_links": needs_extra_links_list,
"needs_extra_options": needs_extra_options,
"needs_graph_check": graph_check_dict,
}


def default_options() -> list[str]:
"""
Helper function to get a list of all default options defined by
sphinx, sphinx-needs etc.
"""
return [
"target_id",
"id",
"status",
"docname",
"lineno",
"type",
"lineno_content",
"doctype",
"content",
"type_name",
"type_color",
"type_style",
"title",
"full_title",
"layout",
"template",
"id_parent",
"id_complete",
"external_css",
"sections",
"section_name",
"type_prefix",
"constraints_passed",
"collapse",
"hide",
"delete",
"jinja_content",
"is_part",
"is_need",
"is_external",
"is_modified",
"modifications",
"has_dead_links",
"has_forbidden_dead_links",
"tags",
"arch",
"parts",
]


def setup(app: Sphinx) -> dict[str, str | bool]:
app.add_config_value("external_needs_source", "", rebuild="env")
app.add_config_value("allowed_external_prefixes", [], rebuild="env")
Expand All @@ -428,11 +245,11 @@ def setup(app: Sphinx) -> dict[str, str | bool]:
metamodel = load_metamodel_data()

# Assign everything to Sphinx config
app.config.needs_types = metamodel["needs_types"]
app.config.needs_extra_links = metamodel["needs_extra_links"]
app.config.needs_extra_options = metamodel["needs_extra_options"]
app.config.graph_checks = metamodel["needs_graph_check"]
app.config.prohibited_words_checks = metamodel["prohibited_words_checks"]
app.config.needs_types = metamodel.needs_types
app.config.needs_extra_links = metamodel.needs_extra_links
app.config.needs_extra_options = metamodel.needs_extra_options
app.config.graph_checks = metamodel.needs_graph_check
app.config.prohibited_words_checks = metamodel.prohibited_words_checks

# app.config.stop_words = metamodel["stop_words"]
# app.config.weak_words = metamodel["weak_words"]
Expand Down
30 changes: 30 additions & 0 deletions src/extensions/score_metamodel/metamodel_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# *******************************************************************************
# 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 dataclasses import dataclass, field

from sphinx_needs.config import NeedType


@dataclass
class ProhibitedWordCheck:
name: str
option_check: dict[str, list[str]] = field(
default_factory=dict
) # { Option: [Forbidden words]}
types: list[str] = field(default_factory=list)


class ScoreNeedType(NeedType):
tags: list[str]
parts: int
41 changes: 19 additions & 22 deletions src/extensions/score_metamodel/tests/test_metamodel_load.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,49 +33,46 @@ def test_load_metamodel_data():
result = load_metamodel_data()

# Assertions
assert "needs_types" in result
assert len(result["needs_types"]) == 1
assert result["needs_types"][0]["directive"] == "type1"
assert result["needs_types"][0]["title"] == "Type 1"
assert result["needs_types"][0]["prefix"] == "T1"
assert result["needs_types"][0]["color"] == "blue"
assert result["needs_types"][0]["style"] == "bold"
assert result["needs_types"][0]["mandatory_options"] == {"opt1": "value1"}
assert result["needs_types"][0]["opt_opt"] == {
assert len(result.needs_types) == 1
assert result.needs_types[0]["directive"] == "type1"
assert result.needs_types[0]["title"] == "Type 1"
assert result.needs_types[0]["prefix"] == "T1"
assert result.needs_types[0]["color"] == "blue"
assert result.needs_types[0]["style"] == "bold"
assert result.needs_types[0]["mandatory_options"] == {"opt1": "value1"}
assert result.needs_types[0]["opt_opt"] == {
"opt2": "value2",
"opt3": "value3",
"global_opt": "global_value",
}
assert result["needs_types"][0]["req_link"] == [("link1", "value1")]
assert result["needs_types"][0]["opt_link"] == [("link2", "value2")]
assert result.needs_types[0]["req_link"] == [("link1", "value1")]
assert result.needs_types[0]["opt_link"] == [("link2", "value2")]

assert "needs_extra_links" in result
assert len(result["needs_extra_links"]) == 1
assert result["needs_extra_links"][0] == {
assert len(result.needs_extra_links) == 1
assert result.needs_extra_links[0] == {
"option": "link_option1",
"incoming": "incoming1",
"outgoing": "outgoing1",
}

assert "needs_extra_options" in result
assert result["needs_extra_options"] == ["global_opt", "opt1", "opt2", "opt3"]
assert result.needs_extra_options == ["global_opt", "opt1", "opt2", "opt3"]

assert "prohibited_words_checks" in result
assert result["prohibited_words_checks"][0] == ProhibitedWordCheck(
assert result.prohibited_words_checks[0] == ProhibitedWordCheck(
name="title_check", option_check={"title": ["stop_word1"]}
)

assert result["prohibited_words_checks"][1] == ProhibitedWordCheck(
assert result.prohibited_words_checks[1] == ProhibitedWordCheck(
name="content_check",
option_check={"content": ["weak_word1"]},
types=["req_type"],
)

assert "needs_graph_check" in result
assert result["needs_graph_check"]["needs_graph_check"]["needs"] == {
defined_graph_check = result.needs_graph_check["needs_graph_check"]
assert isinstance(defined_graph_check, dict)
assert defined_graph_check["needs"] == {
"include": "type1",
"condition": "opt1 == test",
}
assert result["needs_graph_check"]["needs_graph_check"]["check"] == {
assert defined_graph_check["check"] == {
"link1": "opt1 == test",
}
Loading