mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-11-03 12:53:35 +08:00
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.
222 lines
6.5 KiB
Python
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()
|