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
37 changes: 14 additions & 23 deletions src/extensions/score_metamodel/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,17 +143,18 @@ def is_check_enabled(check: local_check_function | graph_check_function):
logger.debug(f"Running graph check {check} for all needs")
check(app, needs_all_needs, log)

if log.has_warnings:
logger.warning("Some needs have issues. See the log for more information.")
if log.warnings:
logger.warning(
f"{log.warnings} needs have issues. See the log for more information."
)

if log.has_infos:
if log.infos:
log.flush_new_checks()
logger.info(
"\n\nThese next warnings are displayed as info statements for now. "
"They will become real warnings in the future. "
f"\nThe {log.infos} warnings above are non fatal for now. "
"They will become fatal in the future. "
"Please fix them as soon as possible.\n"
)
# TODO: exit code


def _remove_prefix(word: str, prefixes: list[str]) -> str:
Expand All @@ -163,16 +164,11 @@ def _remove_prefix(word: str, prefixes: list[str]) -> str:
return word


def _get_need_type_for_need(app: Sphinx, need: NeedsInfoType):
need_type = None
def _get_need_type_for_need(app: Sphinx, need: NeedsInfoType) -> ScoreNeedType:
for nt in app.config.needs_types:
try:
if nt["directive"] == need["type"]:
need_type = nt
break
except Exception:
continue
return need_type
if nt["directive"] == need["type"]:
return nt
raise ValueError(f"Need type {need['type']} not found in needs_types")


def _validate_external_need_opt_links(
Expand Down Expand Up @@ -224,15 +220,10 @@ def _check_external_optional_link_patterns(app: Sphinx, log: CheckLogger) -> Non

for need in needs_external_needs.values():
need_type = _get_need_type_for_need(app, need)
if not need_type:
continue

opt_links = dict(need_type.get("opt_link", []))
if not opt_links:
continue

allowed_prefixes = app.config.allowed_external_prefixes
_validate_external_need_opt_links(need, opt_links, allowed_prefixes, log)
if opt_links := need_type["optional_links"]:
allowed_prefixes = app.config.allowed_external_prefixes
_validate_external_need_opt_links(need, opt_links, allowed_prefixes, log)


def setup(app: Sphinx) -> dict[str, str | bool]:
Expand Down
83 changes: 30 additions & 53 deletions src/extensions/score_metamodel/checks/check_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,6 @@
from sphinx.application import Sphinx
from sphinx_needs.data import NeedsInfoType

FieldCheck = tuple[dict[str, str], bool]
CheckingDictType = dict[str, list[FieldCheck]]


def get_need_type(needs_types: list[ScoreNeedType], directive: str) -> ScoreNeedType:
for need_type in needs_types:
Expand Down Expand Up @@ -142,41 +139,28 @@ def check_options(
"""
production_needs_types = app.config.needs_types

try:
need_options = get_need_type(production_needs_types, need["type"])
except ValueError:
log.warning_for_option(need, "type", "no type info defined for semantic check.")
return

if not need_options.get("mandatory_options", {}):
log.warning_for_option(need, "type", "no type info defined for semantic check.")
return

# Validate Options and Links
checking_dict: CheckingDictType = {
"option": [
(need_options.get("mandatory_options", {}), True),
(need_options.get("opt_opt", {}), False),
],
"link": [
(dict(need_options.get("req_link", [])), True),
(dict(need_options.get("opt_link", [])), False),
],
}
need_options = get_need_type(production_needs_types, need["type"])

# If undefined this is an empty list
allowed_prefixes = app.config.allowed_external_prefixes

for field_type, check_fields in checking_dict.items():
for field_values, is_required in check_fields:
validate_fields(
need,
log,
field_values,
required=is_required,
field_type=field_type,
allowed_prefixes=allowed_prefixes,
)
# Validate Options and Links
field_validations = [
("option", need_options["mandatory_options"], True),
("option", need_options["optional_options"], False),
("link", need_options["mandatory_links"], True),
("link", need_options["optional_links"], False),
]

for field_type, field_values, is_required in field_validations:
validate_fields(
need,
log,
field_values,
required=is_required,
field_type=field_type,
allowed_prefixes=allowed_prefixes,
)


@local_check
Expand All @@ -193,25 +177,18 @@ def check_extra_options(

production_needs_types = app.config.needs_types
default_options_list = default_options()
try:
need_options = get_need_type(production_needs_types, need["type"])
except ValueError:
msg = "no type info defined for semantic check."
log.warning_for_option(need, "type", msg)
return

required_options: dict[str, str] = need_options.get("mandatory_options", {})
optional_options: dict[str, str] = need_options.get("opt_opt", {})
required_links: list[str] = [x[0] for x in need_options.get("req_link", ())]
optional_links: list[str] = [x[0] for x in need_options.get("opt_link", ())]

allowed_options = (
list(required_options.keys())
+ list(optional_options.keys())
+ required_links
+ optional_links
+ default_options_list
)
need_options = get_need_type(production_needs_types, need["type"])

# list() creates a copy to avoid modifying the original
allowed_options = list(default_options_list)

for o in (
"mandatory_options",
"optional_options",
"mandatory_links",
"optional_links",
):
allowed_options.extend(need_options[o].keys())

extra_options = [
option
Expand Down
15 changes: 6 additions & 9 deletions src/extensions/score_metamodel/log.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,12 @@ def warning(
self._log.warning(msg, type="score_metamodel", location=location)

@property
def has_warnings(self):
return self._warning_count > 0
def warnings(self):
return self._warning_count

@property
def has_infos(self):
return self._info_count > 0
def infos(self):
return self._info_count

def flush_new_checks(self):
"""Log all new-check messages together at once."""
Expand All @@ -108,14 +108,11 @@ def make_header_line(text: str, width: int = 80) -> str:
if not self._new_checks:
return

info_header = make_header_line("[INFO MESSAGE]")
separator = "=" * 80
warning_header = make_header_line(
f"[New Checks] has {len(self._new_checks)} warnings"
f"{len(self._new_checks)} non-fatal warnings "
"(will become fatal in the future)"
)

logger.info(info_header)
logger.info(separator)
logger.info(warning_header)

for msg, location in self._new_checks:
Expand Down
4 changes: 4 additions & 0 deletions src/extensions/score_metamodel/metamodel_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,7 @@ class ProhibitedWordCheck:
class ScoreNeedType(NeedType):
tags: list[str]
parts: int
mandatory_options: dict[str, str]
optional_options: dict[str, str]
mandatory_links: dict[str, str]
optional_links: dict[str, str]
4 changes: 2 additions & 2 deletions src/extensions/score_metamodel/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,14 @@ def __init__(self):
super().__init__(self._mock_logger, app_path)

def assert_no_warnings(self):
if self.has_warnings:
if self.warnings:
warnings = "\n".join(
f"* {call}" for call in self._mock_logger.warning.call_args_list
)
pytest.fail(f"Expected no warnings, but got:\n{warnings}")

def assert_no_infos(self):
if self.has_infos:
if self.infos:
infos = "\n".join(
f"* {call}" for call in self._mock_logger.info.call_args_list
)
Expand Down
Loading