Files
galaxy/test/unit/util/test_utils.py
T
paperbenni bec3d3ad9c Drop support for Python 3.8
Drop Python 3.8 support in 5 Pulsar-compatible packages
(job_metrics, tool_util, tool_util_models, util, objectstore)
and run pyupgrade --py310-plus on their source code.

- Bump requires-python from >=3.8 to >=3.10 in all 5 packages
- Remove Python 3.8 and 3.9 classifiers
- Remove ruff per-file-ignores for UP rules on these paths
- Remove backports.zoneinfo conditional dependency
- Remove pydyf<0.11 pin from conditional-requirements.txt
- Update Makefile pyupgrade target (remove PY38_PYUPGRADE_PATHS)
- Update CI workflow to test with Python 3.10 instead of 3.8

Clean up unused deprecated typing imports after pyupgrade

Remove now-unused typing imports (Dict, List, Optional, Set, Tuple,
Type, Union) that became dead after pyupgrade --py310-plus converted
annotations to use built-in types and | syntax.

Also run ruff check --fix --select=UP007,UP045 across the entire
codebase to convert remaining Optional[X] -> X | None and
Union[X, Y] -> X | Y patterns.

Enable ruff UP007/UP045 for Python 3.10 union syntax

Remove UP007 (Union[X,Y] -> X | Y) and UP045 (Optional[X] -> X | None)
from the ruff ignore list and convert all type aliases across the
codebase. These rules were deferred while Python 3.9 was supported;
requires-python is now >=3.10.

A custom script was used because neither ruff --fix nor
pyupgrade --py310-plus converts Optional[X]/Union[X,Y] in type alias
positions (e.g. X = Union[A, B]) — they only handle annotation
positions (e.g. def f(x: Optional[int])). All 167 violations were
module-level type aliases. A few edge cases were fixed manually:
single-element Union[X,], typing.Union qualified refs, runtime
Optional[type] calls, and Annotated[Optional[...]] pydantic fields.

Fix UP007 autofix regression with string forward reference type aliases

Commit fa6bd955a0 enabled ruff UP007/UP045 and auto-fixed
module-level type aliases using Union with string forward
references, producing invalid 'str | str' expressions.

This was a known ruff bug (charliermarsh/ruff#826) that has since
been fixed in later ruff versions, but this codebase was
converted before the fix was in place.

Revert to Union syntax and restore TYPE_CHECKING imports that
ruff's TCH rule cleaned up as a side effect when it thought the
forward references were unused.
2026-06-25 15:12:05 +01:00

222 lines
6.5 KiB
Python

import errno
import os
import tempfile
from enum import Enum
from io import StringIO
from pathlib import Path
import pytest
from galaxy import util
from galaxy.util.json import safe_loads
SECTION_XML = """<?xml version="1.0" ?>
<section id="fasta_fastq_manipulation" name="Fasta Fastq Manipulation" version="">
<tool file="toolshed.g2.bx.psu.edu/repos/peterjc/seq_filter_by_id/fb1313d79396/seq_filter_by_id/tools/seq_filter_by_id/seq_filter_by_id.xml" guid="toolshed.g2.bx.psu.edu/repos/peterjc/seq_filter_by_id/seq_filter_by_id/0.2.5">
<tool_shed>
toolshed.g2.bx.psu.edu
</tool_shed>
</tool>
</section>
"""
def test_strip_control_characters():
s = "\x00bla"
assert util.strip_control_characters(s) == "bla"
def test_parse_xml_string():
section = util.parse_xml_string(SECTION_XML)
_verify_section(section)
def test_parse_xml_file():
with tempfile.NamedTemporaryFile(mode="w") as tmp:
tmp.write(SECTION_XML)
tmp.flush()
section = util.parse_xml(tmp.name).getroot()
_verify_section(section)
def _verify_section(section):
tool = next(iter(section))
assert sorted(tool.items()) == [
(
"file",
"toolshed.g2.bx.psu.edu/repos/peterjc/seq_filter_by_id/fb1313d79396/seq_filter_by_id/tools/seq_filter_by_id/seq_filter_by_id.xml",
),
("guid", "toolshed.g2.bx.psu.edu/repos/peterjc/seq_filter_by_id/seq_filter_by_id/0.2.5"),
]
assert next(iter(tool)).text == "toolshed.g2.bx.psu.edu"
def test_xml_to_string():
section = util.parse_xml_string(SECTION_XML)
s = util.xml_to_string(section)
assert len(s.split("\n")) == 1
def test_xml_to_string_pretty():
section = util.parse_xml_string(SECTION_XML)
s = util.xml_to_string(section, pretty=True)
PRETTY = """<?xml version="1.0" ?>
<section id="fasta_fastq_manipulation" name="Fasta Fastq Manipulation" version="">
<tool file="toolshed.g2.bx.psu.edu/repos/peterjc/seq_filter_by_id/fb1313d79396/seq_filter_by_id/tools/seq_filter_by_id/seq_filter_by_id.xml" guid="toolshed.g2.bx.psu.edu/repos/peterjc/seq_filter_by_id/seq_filter_by_id/0.2.5">
<tool_shed>toolshed.g2.bx.psu.edu</tool_shed>
</tool>
</section>"""
assert s == PRETTY
def test_parse_xml_enoent():
with tempfile.NamedTemporaryFile() as temp:
path = temp.name
with pytest.raises(IOError) as excinfo:
util.parse_xml(path)
assert excinfo.value.errno == errno.ENOENT
def test_clean_multiline_string():
x = util.clean_multiline_string("""
a
b
c
""")
assert x == "a\nb\nc\n"
def test_iter_start_of_lines():
assert list(util.iter_start_of_line(StringIO("\n1\n\n12\n123\n1234\n"), 1)) == ["\n", "1", "\n", "1", "1", "1"]
def test_safe_loads():
d: dict[str, str] = {}
rval = safe_loads(d)
assert rval == d
assert rval is not d
rval["foo"] = "bar"
assert "foo" not in d
s = '{"foo": "bar"}'
assert safe_loads(s) == {"foo": "bar"}
def test_in_packages(monkeypatch):
util_path = Path(util.__file__).parent
assert util.in_packages() == (not str(util_path).endswith("lib/galaxy/util"))
def test_galaxy_directory(monkeypatch):
galaxy_dir = util.galaxy_directory()
assert os.path.isabs(galaxy_dir)
assert os.path.isfile(os.path.join(galaxy_dir, "run.sh"))
def test_listify() -> None:
assert util.listify(None) == []
assert util.listify(False) == []
assert util.listify(True) == [True]
assert util.listify("foo") == ["foo"]
assert util.listify("foo, bar") == ["foo", " bar"]
assert util.listify("foo, bar", do_strip=True) == ["foo", "bar"]
list_ = [1, 2, 3]
assert util.listify(list_) is list_
assert util.listify((1, 2, 3)) == [1, 2, 3]
s = {1, 2, 3}
assert util.listify(s) == [s]
d = {"a": 1, "b": 2, "c": 3}
assert util.listify(d) == [d]
o = object()
assert util.listify(o) == [o]
def test_enum_values():
class Stuff(str, Enum):
A = "a"
C = "c"
B = "b"
assert util.enum_values(Stuff) == ["a", "c", "b"]
DOI_VALID_VALUES = [
"https://doi.org/10.1234/42",
"doi.org/10.1234/42",
"doi:10.1234/42",
"doi:10.1234567890/42", # longer prefix
"doi:10.1234/42ab:%&*$//crazy-suffix/%/&/",
"doi:10.1234/aa",
]
@pytest.mark.parametrize("input", DOI_VALID_VALUES)
def test_validate_doi_pass(input):
assert util.validate_doi(input)
DOI_INVALID_VALUES = [
"http://doi.org/10.1234/42",
"invalid:10.1234/42",
"doi:11.1234/42",
"doi:101234/42",
"doi:10. 1234/42",
"doi:10.abc/42",
"doi:10.1234/ 42",
"doi:10.1234/42/a b",
]
@pytest.mark.parametrize("input", DOI_INVALID_VALUES)
def test_validate_doi_fail(input):
assert not util.validate_doi(input)
def test_validate_doi_fail_too_long():
long_suffix = "a" * (util.DOI_MAX_LENGTH - 12)
doi = f"doi:10.1000/{long_suffix}"
assert util.validate_doi(doi)
assert not util.validate_doi(doi + "a") # Increase length by 1 past max limit
@pytest.mark.parametrize(
"input_name,expected_output",
[
# Existing documented behavior
("My Cool Object", "My-Cool-Object"),
("!My Cool Object!", "My-Cool-Object"),
("Hello\u20a9\u25ce\u0491\u029f\u217e", "Hello"),
# Additional edge cases
("simple", "simple"),
("UPPERCASE", "UPPERCASE"), # Note: lowercase applied separately
("with-dash", "with-dash"),
("with spaces", "with-spaces"),
(" multiple spaces ", "-multiple-spaces"),
("trailing!", "trailing"),
("!leading", "leading"),
("special@#$chars", "specialchars"),
("parentheses(test)", "parenthesestest"),
("Rincewind (Ankh-Morpork)", "Rincewind-Ankh-Morpork"),
],
)
def test_ready_name_for_url(input_name, expected_output):
"""Test that ready_name_for_url correctly sanitizes names for URL use."""
assert util.ready_name_for_url(input_name) == expected_output
@pytest.mark.parametrize(
"target,expected_substring",
[
("normal.txt", 'filename="normal.txt"'),
("file.gz ", 'filename="file.gz"'),
(" file.gz", 'filename="file.gz"'),
(" file.gz ", 'filename="file.gz"'),
("Galaxy102-[name].fastqsanger.gz ", 'filename="Galaxy102-[name].fastqsanger.gz"'),
],
)
def test_to_content_disposition(target, expected_substring):
result = util.to_content_disposition(target)
assert result.startswith("attachment; ")
assert expected_substring in result
# Ensure no trailing whitespace in the header value
assert result == result.strip()