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
316 changes: 210 additions & 106 deletions src/extensions/score_draw_uml_funcs/__init__.py

Large diffs are not rendered by default.

8 changes: 0 additions & 8 deletions src/extensions/score_metamodel/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
# SPDX-License-Identifier: Apache-2.0
# *******************************************************************************
import importlib
import json
import os
import pkgutil
from collections.abc import Callable
Expand All @@ -24,13 +23,6 @@
from sphinx_needs.config import NeedType
from sphinx_needs.data import NeedsInfoType, NeedsView, SphinxNeedsData

from src.helper_lib import (
find_git_root,
find_ws_root,
get_current_git_hash,
get_github_repo_info,
)

from .external_needs import connect_external_needs
from .log import CheckLogger

Expand Down
9 changes: 7 additions & 2 deletions src/extensions/score_metamodel/checks/attributes_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,15 @@ def check_id_length(app: Sphinx, need: NeedsInfoType, log: CheckLogger):
if parts[1] == "example_feature":
max_lenght += 17 # _example_feature_
if len(need["id"]) > max_lenght:
length = 0
if "example_feature" not in need["id"]:
length = len(need["id"])
else:
length = len(need["id"]) - 17
Comment on lines +73 to +77
Copy link
Copy Markdown
Contributor

@MaximilianSoerenPollak MaximilianSoerenPollak Aug 15, 2025

Choose a reason for hiding this comment

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

Nit fix in next pr:

Suggested change
length = 0
if "example_feature" not in need["id"]:
length = len(need["id"])
else:
length = len(need["id"]) - 17
length = len(need["id"])
if "example_feature" in need["id"]:
length -= 17

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

msg = (
f"exceeds the maximum allowed length of 45 characters "
"(current length: "
f"{len(need['id']) if 'example_feature' not in need['id'] else len(need['id']) - 17})."
f"{length})."
)
log.warning_for_option(need, "id", msg)

Expand All @@ -82,7 +87,7 @@ def _check_options_for_prohibited_words(
prohibited_word_checks: ProhibitedWordCheck, need: NeedsInfoType, log: CheckLogger
):
options: list[str] = [
x for x in prohibited_word_checks.option_check.keys() if x != "types"
x for x in prohibited_word_checks.option_check if x != "types"
]
for option in options:
forbidden_words = prohibited_word_checks.option_check[option]
Expand Down
54 changes: 33 additions & 21 deletions src/extensions/score_metamodel/checks/check_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
# SPDX-License-Identifier: Apache-2.0
# *******************************************************************************
import re
from os import error

from score_metamodel import (
CheckLogger,
Expand All @@ -33,6 +34,30 @@ def get_need_type(needs_types: list[ScoreNeedType], directive: str) -> ScoreNeed
raise ValueError(f"Need type {directive} not found in needs_types")


def _normalize_values(raw_value: str | list[str] | None) -> list[str]:
"""Normalize a raw value into a list of strings."""
Comment thread
MaximilianSoerenPollak marked this conversation as resolved.
if raw_value is None:
return []
if isinstance(raw_value, str):
return [raw_value]
if isinstance(raw_value, list) and all(isinstance(v, str) for v in raw_value):
return raw_value
raise ValueError


def _validate_value_pattern(
value: str, pattern: str, need: NeedsInfoType, field: str, log: CheckLogger
) -> None:
"""Check if a value matches the given pattern, log warnings if not."""
try:
if not re.match(pattern, value):
log.warning_for_option(need, field, f"does not follow pattern `{pattern}`.")
except TypeError:
log.warning_for_option(
need, field, f"pattern `{pattern}` is not a valid regex pattern."
)


def validate_fields(
need: NeedsInfoType,
log: CheckLogger,
Expand Down Expand Up @@ -64,31 +89,18 @@ def remove_prefix(word: str, prefixes: list[str]) -> str:
need, f"is missing required {field_type}: `{field}`."
)
continue # Skip empty optional fields

values: list[str]

if isinstance(raw_value, str):
values = [raw_value]
elif isinstance(raw_value, list) and all(isinstance(v, str) for v in raw_value):
values = raw_value
else:
values = [str(raw_value)]

try:
values = _normalize_values(raw_value)
except ValueError as err:
raise ValueError(
f"An Attribute inside need {need['id']} is "
"not of type str. Only Strings are allowed"
) from err
Comment on lines +92 to +98
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit (fix in next PR):

Add comment explaining why we did this.

Suggested change
try:
values = _normalize_values(raw_value)
except ValueError as err:
raise ValueError(
f"An Attribute inside need {need['id']} is "
"not of type str. Only Strings are allowed"
) from err
# Try except used to add more context to Error without passing variables just for that to function
try:
values = _normalize_values(raw_value)
except ValueError as err:
raise ValueError(
f"An Attribute inside need {need['id']} is "
"not of type str. Only Strings are allowed"
) from err

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

# The filter ensures that the function is only called when needed.
for value in values:
if allowed_prefixes:
value = remove_prefix(value, allowed_prefixes)
try:
if not re.match(pattern, value):
log.warning_for_option(
need, field, f"does not follow pattern `{pattern}`."
)
except TypeError:
log.warning_for_option(
need,
field,
f"pattern `{pattern}` is not a valid regex pattern.",
)
_validate_value_pattern(value, pattern, need, field, log)


# req-Id: tool_req__docs_req_attr_reqtype
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ def test_invalid_option_type(self):
target_id="wf_req__001",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit (fix in next PR):

Add a test that now tests the new 'value error' path we have introduced

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

id="wf_req__001",
type="workflow",
some_invalid_option=42,
some_invalid_option="42",
docname=None,
lineno=None,
)
Expand Down
7 changes: 5 additions & 2 deletions src/extensions/score_source_code_linker/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ def setup_once(app: Sphinx, config: Config):
LOGGER.debug(f"DEBUG: Git root is {find_git_root()}")

# Run only for local files!
# ws_root is not set when running on any on bazel run command repositories (dependencies)
# ws_root is not set when running on any on bazel run
# command repositories (dependencies)
ws_root = find_ws_root()
if not ws_root:
return
Expand Down Expand Up @@ -146,7 +147,9 @@ def group_by_need(source_code_links: list[NeedLink]) -> dict[str, list[NeedLink]
return source_code_links_by_need


def get_github_link(needlink: NeedLink = DefaultNeedLink()) -> str:
def get_github_link(needlink: NeedLink | None = None) -> str:
if needlink is None:
needlink = DefaultNeedLink()
passed_git_root = find_git_root()
if passed_git_root is None:
passed_git_root = Path()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,7 @@ def test_get_current_git_hash(git_repo):

def test_get_current_git_hash_invalid_repo(temp_dir):
"""Test getting git hash from invalid repository."""
with pytest.raises(Exception):
with pytest.raises(subprocess.CalledProcessError):
get_current_git_hash(temp_dir)


Expand Down Expand Up @@ -519,7 +519,7 @@ def test_group_by_need_and_find_need_integration(sample_needlinks):
)

# Test finding needs for each group
for need_id, links in grouped.items():
for need_id in grouped:
found_need = find_need(all_needs, need_id, ["PREFIX_"])
if need_id in ["TREQ_ID_1", "TREQ_ID_2"]:
assert found_need is not None
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#
# SPDX-License-Identifier: Apache-2.0
# *******************************************************************************
import contextlib
import json
import os
import shutil
Expand Down Expand Up @@ -150,14 +151,11 @@ def _create_app():
base_dir = sphinx_base_dir
docs_dir = base_dir / "docs"

original_cwd = None
# CRITICAL: Change to a directory that exists and is accessible
# This fixes the "no such file or directory" error in Bazel
original_cwd = None
try:
with contextlib.suppress(FileNotFoundError):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit (fix in next PR):

Comment what this does briefly as people might not be familliar with this.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I kind of disagree. We should not explain standard python functionality. Python programmers must know python.
(But we should explain why we ignore the exception)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

That does make more sense, true. Let's do that.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

original_cwd = os.getcwd()
except FileNotFoundError:
# Current working directory doesn't exist, which is the problem
pass

# Change to the base_dir before creating SphinxTestApp
os.chdir(base_dir)
Expand All @@ -173,11 +171,9 @@ def _create_app():
finally:
# Try to restore original directory, but don't fail if it doesn't exist
if original_cwd is not None:
try:
# Original directory might not exist anymore in Bazel sandbox
with contextlib.suppress(FileNotFoundError, OSError):
os.chdir(original_cwd)
except (FileNotFoundError, OSError):
# Original directory might not exist anymore in Bazel sandbox
pass

return _create_app

Expand Down
47 changes: 23 additions & 24 deletions src/find_runfiles/test_find_runfiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,25 +32,25 @@ def get_runfiles_dir_impl(
def test_run_incremental():
"""bazel run //process-docs:incremental"""
# in incremental.py:
assert (
get_runfiles_dir_impl(
cwd="/home/vscode/.cache/bazel/_bazel_vscode/6084288f00f33db17acb4220ce8f1999/execroot/_main/bazel-out/k8-fastbuild/bin/process-docs/incremental.runfiles/_main",
conf_dir="process-docs",
env_runfiles="/home/vscode/.cache/bazel/_bazel_vscode/6084288f00f33db17acb4220ce8f1999/execroot/_main/bazel-out/k8-fastbuild/bin/process-docs/incremental.runfiles",
git_root="/workspaces/process",
)
== "/workspaces/process/bazel-out/k8-fastbuild/bin/process-docs/incremental.runfiles"
assert get_runfiles_dir_impl(
cwd="/home/vscode/.cache/bazel/_bazel_vscode/6084288f00f33db17acb4220ce8f1999/execroot/_main/bazel-out/k8-fastbuild/bin/process-docs/incremental.runfiles/_main",
conf_dir="process-docs",
env_runfiles="/home/vscode/.cache/bazel/_bazel_vscode/6084288f00f33db17acb4220ce8f1999/execroot/_main/bazel-out/k8-fastbuild/bin/process-docs/incremental.runfiles",
git_root="/workspaces/process",
) == (
"/workspaces/process/bazel-out/k8-fastbuild/bin/process-docs/"
"incremental.runfiles"
)

# in conf.py:
assert (
get_runfiles_dir_impl(
cwd="/workspaces/process/process-docs",
conf_dir="process-docs",
env_runfiles="/home/vscode/.cache/bazel/_bazel_vscode/6084288f00f33db17acb4220ce8f1999/execroot/_main/bazel-out/k8-fastbuild/bin/process-docs/incremental.runfiles",
git_root="/workspaces/process",
)
== "/workspaces/process/bazel-out/k8-fastbuild/bin/process-docs/incremental.runfiles"
assert get_runfiles_dir_impl(
cwd="/workspaces/process/process-docs",
conf_dir="process-docs",
env_runfiles="/home/vscode/.cache/bazel/_bazel_vscode/6084288f00f33db17acb4220ce8f1999/execroot/_main/bazel-out/k8-fastbuild/bin/process-docs/incremental.runfiles",
git_root="/workspaces/process",
) == (
"/workspaces/process/bazel-out/k8-fastbuild/bin/process-docs/"
"incremental.runfiles"
)


Expand Down Expand Up @@ -83,12 +83,11 @@ def test_esbonio_old():
def test3():
# docs named differently, just to make sure nothing is hardcoded
# bazel run //other-docs:incremental
assert (
get_runfiles_dir_impl(
cwd="/workspaces/process/other-docs",
conf_dir="other-docs",
env_runfiles="/home/vscode/.cache/bazel/_bazel_vscode/6084288f00f33db17acb4220ce8f1999/execroot/_main/bazel-out/k8-fastbuild/bin/other-docs/incremental.runfiles",
git_root="/workspaces/process",
)
== "/workspaces/process/bazel-out/k8-fastbuild/bin/other-docs/incremental.runfiles"
assert get_runfiles_dir_impl(
cwd="/workspaces/process/other-docs",
conf_dir="other-docs",
env_runfiles="/home/vscode/.cache/bazel/_bazel_vscode/6084288f00f33db17acb4220ce8f1999/execroot/_main/bazel-out/k8-fastbuild/bin/other-docs/incremental.runfiles",
git_root="/workspaces/process",
) == (
"/workspaces/process/bazel-out/k8-fastbuild/bin/other-docs/incremental.runfiles"
)
2 changes: 1 addition & 1 deletion src/helper_lib/test_helper_lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def test_git_operations_with_no_commits(temp_dir):

os.chdir(Path(git_dir).absolute())
# Should raise an exception when trying to get hash
with pytest.raises(Exception):
with pytest.raises(subprocess.CalledProcessError):
get_current_git_hash(git_dir)


Expand Down
70 changes: 34 additions & 36 deletions src/tests/test_consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ def sphinx_base_dir(tmp_path_factory: TempPathFactory, pytestconfig) -> Path:
temp_dir = tmp_path_factory.mktemp("testing_dir")
print(f"[blue]Using temporary directory: {temp_dir}[/blue]")
return temp_dir

CACHE_DIR.mkdir(parents=True, exist_ok=True)
print(f"[green]Using persistent cache directory: {CACHE_DIR}[/green]")
return CACHE_DIR
Expand Down Expand Up @@ -210,11 +211,10 @@ def parse_bazel_output(BR: BuildOutput, pytestconfig) -> BuildOutput:
split_warnings = [x for x in err_lines if "WARNING: " in x]
warning_dict: dict[str, list[str]] = defaultdict(list)

if pytestconfig.get_verbosity() >= 2:
if os.getenv("CI"):
print("[DEBUG] Raw warnings in CI:")
for i, warning in enumerate(split_warnings):
print(f"[DEBUG] Warning {i}: {repr(warning)}")
if pytestconfig.get_verbosity() >= 2 and os.getenv("CI"):
print("[DEBUG] Raw warnings in CI:")
for i, warning in enumerate(split_warnings):
print(f"[DEBUG] Warning {i}: {repr(warning)}")

for raw_warning in split_warnings:
# In the CLI we seem to have some ansi codes in the warnings.
Expand Down Expand Up @@ -479,46 +479,44 @@ def setup_test_environment(sphinx_base_dir, pytestconfig):
"""Set up the test environment and return necessary paths and metadata."""
git_root = find_git_root()
if git_root is None:
assert False, "Git root was none"
raise RuntimeError("Git root was not found")
Comment on lines 481 to +482
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit (fix enxt pr):

Suggested change
if git_root is None:
assert False, "Git root was none"
raise RuntimeError("Git root was not found")
assert git_root is None, "Git root was not found"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed.


gh_url = get_github_base_url()
current_hash = get_current_git_commit(git_root)

os.chdir(Path(sphinx_base_dir).absolute())

verbosity = pytestconfig.get_verbosity()

if verbosity >= 2:
print(f"[DEBUG] git_root: {git_root}")
def debug_print(message):
if verbosity >= 2:
print(f"[DEBUG] {message}")

# Get GitHub URL and current hash for git override
debug_print(f"git_root: {git_root}")

if verbosity >= 2:
print(f"[DEBUG] gh_url: {gh_url}")
print(f"[DEBUG] current_hash: {current_hash}")
print(
"[DEBUG] Working directory has uncommitted changes: "
f"{has_uncommitted_changes(git_root)}"
)

# Create symlink for local docs-as-code
docs_as_code_dest = sphinx_base_dir / "docs_as_code"
if docs_as_code_dest.exists() or docs_as_code_dest.is_symlink():
# Remove existing symlink/directory to recreate it
if docs_as_code_dest.is_symlink():
docs_as_code_dest.unlink()
if verbosity >= 2:
print(f"[DEBUG] Removed existing symlink: {docs_as_code_dest}")
elif docs_as_code_dest.is_dir():
import shutil

shutil.rmtree(docs_as_code_dest)
if verbosity >= 2:
print(f"[DEBUG] Removed existing directory: {docs_as_code_dest}")

docs_as_code_dest.symlink_to(git_root)
# Get GitHub URL and current hash for git override
debug_print(f"gh_url: {gh_url}")
debug_print(f"current_hash: {current_hash}")
debug_print(
"Working directory has uncommitted changes: "
f"{has_uncommitted_changes(git_root)}"
)

if verbosity >= 2:
print(f"[DEBUG] Symlink created: {docs_as_code_dest} -> {git_root}")
def recreate_symlink(dest, target):
# Create symlink for local docs-as-code
if dest.exists() or dest.is_symlink():
# Remove existing symlink/directory to recreate it
if dest.is_symlink():
dest.unlink()
debug_print(f"Removed existing symlink: {dest}")
elif dest.is_dir():
import shutil

shutil.rmtree(dest)
debug_print(f"Removed existing directory: {dest}")
dest.symlink_to(target)
debug_print(f"Symlink created: {dest} -> {target}")

recreate_symlink(sphinx_base_dir / "docs_as_code", git_root)

return gh_url, current_hash

Expand Down
Loading