Files
galaxy/scripts/release-diff.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

182 lines
4.8 KiB
Python

#!/usr/bin/env python
import argparse
import glob
import subprocess
from pathlib import Path
import yaml
def flatten(d, path):
"""
Flatten a dictionary into ('some/path/to/key', value)
>>> flatten({'a': {'b': 2}, 'q': 3}, [])
[('a.b', 2), ('q', 3)]
"""
if isinstance(d, dict):
for k, v in d.items():
yield from flatten(v, path + [k])
else:
yield (".".join(path), d)
def flat_dict(d):
return dict(flatten(d, []))
# Load without the includes since we can't follow those across git revisions.
class MockOrderedLoader(yaml.SafeLoader):
def include(self, node):
return {}
MockOrderedLoader.add_constructor("!include", MockOrderedLoader.include)
def diff_files(old, new):
# Flatten them
old_kv = flat_dict(old)
new_kv = flat_dict(new)
# Compare them
old_k = set(old_kv.keys())
new_k = set(new_kv.keys())
added = []
for item in new_k - old_k:
parent = ".".join(item.split(".")[0:-1])
if parent in new_k and parent not in old_k:
added.append(item)
else:
added.append(parent)
added = set(added)
removed = []
for item in old_k - new_k:
parent = ".".join(item.split(".")[0:-1])
if parent in old_k and parent not in new_k:
removed.append(item)
else:
removed.append(parent)
removed = set(removed)
shared = old_k & new_k
changed = [(k, old_kv[k], new_kv[k]) for k in shared if old_kv[k] != new_kv[k]]
return added, removed, changed
def _report_dict(title, subheading, data, mapper):
print(title)
print("-" * len(title))
print()
print(subheading)
print()
for fn in data:
print(fn)
print("~" * len(fn))
print()
for k in data[fn]:
print(mapper(k))
print()
print()
def _indent(s, by=4):
whitespace = " " * by
s = s if isinstance(s, list) else str(s).splitlines()
return "\n".join(f"{whitespace}{line}" for line in s)
def report_diff(added, changed, removed, new_files):
# Print out report
if added or changed or removed:
print("Configuration Changes")
print("=====================")
print()
if added:
_report_dict("Added", "The following configuration options are new", added, lambda x: f"- {x}")
if changed:
_report_dict(
"Changed",
"The following configuration options have been changed",
changed,
lambda x: (
f"- {x[0]} has changed from\n\n ::\n\n{_indent(x[1])}\n\n to\n\n ::\n\n{_indent(x[2])}\n\n"
),
)
if removed:
_report_dict(
"Removed", "The following configuration options have been completely removed", removed, lambda x: f"- {x}"
)
if new_files:
print("New Configuration Files")
print("-----------------------")
print()
print("The following files are new, or recently converted to yaml")
print()
for k in new_files:
print(f"- ``{k}``")
def load_at_time(path, revision=None):
if revision is not None:
return subprocess.check_output(["git", "show", f"{revision}:{path}"], stderr=subprocess.STDOUT)
else:
with open(path) as handle:
return handle.read()
def main(old_revision, new_revision=None):
globs = (
"config/*.yml.sample",
"lib/galaxy/config/schemas/*schema.yml",
)
files_to_diff = [f for g in globs for f in glob.glob(g)]
added = {}
removed = {}
changed = {}
new_files = []
for file in files_to_diff:
filename = file
if "config_schema.yml" in file:
filename = "config/galaxy.yml.sample:galaxy"
real_path = Path(file).resolve().relative_to(Path.cwd())
try:
old_contents = yaml.load(load_at_time(real_path, old_revision), Loader=MockOrderedLoader)
new_contents = yaml.load(load_at_time(real_path, new_revision), Loader=MockOrderedLoader)
a, r, c = diff_files(old_contents, new_contents)
if a:
added[filename] = sorted(a)
if r:
removed[filename] = sorted(r)
if c:
changed[filename] = sorted(c)
except subprocess.CalledProcessError:
new_files.append(file)
report_diff(added, changed, removed, new_files)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Diff yaml configuration files between two points in time.")
parser.add_argument("old_revision", help="Old revision")
parser.add_argument(
"--new_revision",
help="New revision (defaults to whatever is currently in tree)",
)
args = parser.parse_args()
main(args.old_revision, args.new_revision)