Merge pull request #22061 from mvdbeek/fix_22052_validate_required_files_paths

[26.0] Add linter to validate required_files include paths exist
This commit is contained in:
Marius van den Beek
2026-03-12 14:40:17 +01:00
committed by GitHub
2 changed files with 126 additions and 1 deletions
@@ -0,0 +1,43 @@
import os
from typing import TYPE_CHECKING
from galaxy.tool_util.lint import Linter
from galaxy.tool_util.parser.interface import RequiredFiles
if TYPE_CHECKING:
from galaxy.tool_util.lint import LintContext
from galaxy.tool_util.parser import ToolSource
class RequiredFilesExist(Linter):
"""Check that required_files include patterns match existing files."""
@classmethod
def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
if not tool_source.source_path:
return
tool_dir = os.path.dirname(tool_source.source_path)
required_files = tool_source.parse_required_files()
if required_files is None:
return
for include in required_files.includes:
per_include = RequiredFiles.from_dict(
{
"includes": [include],
"excludes": [],
"extend_default_excludes": False,
}
)
if not per_include.find_required_files(tool_dir):
path = include["path"]
path_type = include.get("path_type", "literal")
if path_type == "literal":
lint_ctx.error(
f"Required file [{path}] does not exist",
linter=cls.name(),
)
else:
lint_ctx.error(
f"Required files pattern [{path}] (type {path_type}) does not match any files",
linter=cls.name(),
)
+83 -1
View File
@@ -21,6 +21,7 @@ from galaxy.tool_util.linters import (
help,
inputs,
output,
required_files,
stdio,
tests,
xml_order,
@@ -2424,7 +2425,7 @@ def test_skip_by_module(lint_ctx):
def test_list_linters():
linter_names = Linter.list_listers()
# make sure to add/remove a test for new/removed linters if this number changes
assert len(linter_names) == 142
assert len(linter_names) == 143
assert "Linter" not in linter_names
# make sure that linters from all modules are available
for prefix in [
@@ -2500,3 +2501,84 @@ def test_linter_module_list():
elif inspect.isclass(value) and issubclass(value, Linter) and not inspect.isabstract(value):
linter_cnt += 1
assert linter_cnt >= old_linters[module_name]
REQUIRED_FILES_LITERAL = """
<tool id="id" name="name">
<required_files>
<include path="my_script.R"/>
</required_files>
</tool>
"""
REQUIRED_FILES_LITERAL_MISSING = """
<tool id="id" name="name">
<required_files>
<include path="nonexistent.py"/>
</required_files>
</tool>
"""
REQUIRED_FILES_GLOB = """
<tool id="id" name="name">
<required_files>
<include path="*.R" type="glob"/>
</required_files>
</tool>
"""
REQUIRED_FILES_GLOB_NO_MATCH = """
<tool id="id" name="name">
<required_files>
<include path="*.py" type="glob"/>
</required_files>
</tool>
"""
def _write_file(directory, filename, content):
"""Write content to a file in directory, return full path."""
path = os.path.join(directory, filename)
with open(path, "w") as f:
f.write(content)
return path
def _load_and_run_lint(lint_ctx, tool_path, lint_module):
"""Load a tool XML from disk and run a lint module on it."""
tool_xml, _ = load_with_references(tool_path)
tool_source = XmlToolSource(tool_xml, source_path=tool_path)
run_lint_module(lint_ctx, lint_module, tool_source)
def test_required_files_literal_exist(lint_ctx):
with tempfile.TemporaryDirectory() as tool_dir:
tool_path = _write_file(tool_dir, "tool.xml", REQUIRED_FILES_LITERAL)
_write_file(tool_dir, "my_script.R", "# R script")
_load_and_run_lint(lint_ctx, tool_path, required_files)
assert not lint_ctx.error_messages
def test_required_files_literal_missing(lint_ctx):
with tempfile.TemporaryDirectory() as tool_dir:
tool_path = _write_file(tool_dir, "tool.xml", REQUIRED_FILES_LITERAL_MISSING)
_load_and_run_lint(lint_ctx, tool_path, required_files)
assert "Required file [nonexistent.py] does not exist" in lint_ctx.error_messages
assert len(lint_ctx.error_messages) == 1
def test_required_files_glob_match(lint_ctx):
with tempfile.TemporaryDirectory() as tool_dir:
tool_path = _write_file(tool_dir, "tool.xml", REQUIRED_FILES_GLOB)
_write_file(tool_dir, "my_script.R", "# R script")
_load_and_run_lint(lint_ctx, tool_path, required_files)
assert not lint_ctx.error_messages
def test_required_files_glob_no_match(lint_ctx):
with tempfile.TemporaryDirectory() as tool_dir:
tool_path = _write_file(tool_dir, "tool.xml", REQUIRED_FILES_GLOB_NO_MATCH)
_write_file(tool_dir, "my_script.R", "# R script")
_load_and_run_lint(lint_ctx, tool_path, required_files)
assert "Required files pattern [*.py] (type glob) does not match any files" in lint_ctx.error_messages
assert len(lint_ctx.error_messages) == 1