plan(P2.3): fix the Python 3.13 compatibility findings in the vendored closure

This commit is contained in:
WildAi
2026-10-23 17:19:26 +03:00
parent 80083c85b4
commit 5dc56e2ce3
2 changed files with 89 additions and 12 deletions
+16 -2
View File
@@ -3,7 +3,6 @@ import math
import torch
import torchvision
import warnings
from distutils.version import LooseVersion
from itertools import repeat
from torch import nn as nn
from torch.nn import functional as F
@@ -14,6 +13,21 @@ from basicsr.ops.dcn import ModulatedDeformConvPack, modulated_deform_conv
from basicsr.utils import get_root_logger
def _version_tuple(version):
"""Numeric version tuple of a version string.
P2.3: replaces the legacy ``LooseVersion`` helper - the standard library
module that provided it was removed in Python 3.12 and this package runs
on Python 3.13. Only the numeric components are compared, which is all
the original call site needs.
"""
parts = []
for chunk in version.split('+')[0].split('.'):
digits = ''.join(char for char in chunk if char.isdigit())
parts.append(int(digits) if digits else 0)
return tuple(parts)
@torch.no_grad()
def default_init_weights(module_list, scale=1, bias_fill=0, **kwargs):
"""Initialize network weights.
@@ -223,7 +237,7 @@ class DCNv2Pack(ModulatedDeformConvPack):
logger = get_root_logger()
logger.warning(f'Offset abs mean is {offset_absmean}, larger than 50.')
if LooseVersion(torchvision.__version__) >= LooseVersion('0.9.0'):
if _version_tuple(torchvision.__version__) >= _version_tuple('0.9.0'):
return torchvision.ops.deform_conv2d(x, offset, self.weight, self.bias, self.stride, self.padding,
self.dilation, mask)
else:
+73 -10
View File
@@ -9,6 +9,9 @@ against the audit document that lists the fix list.
from __future__ import annotations
import importlib.util
import json
import subprocess
import sys
from pathlib import Path
import pytest
@@ -16,6 +19,7 @@ import pytest
REPO_ROOT = Path(__file__).resolve().parent.parent
SCANNER = REPO_ROOT / "tools" / "scan_compat.py"
AUDIT_DOC = REPO_ROOT / "docs" / "audit" / "2026-10-22-compat-scan.md"
DEPS_DIR = REPO_ROOT / "modules" / "deps"
KNOWN_BAD = '''\
import pdb
@@ -131,10 +135,10 @@ def test_finding_line_numbers_are_exact(scanner):
@pytest.mark.unit
def test_real_tree_findings(scanner):
"""The live tree must only contain the findings that are still open.
"""The live tree must not contain any finding any more.
P2.1 removed ``from re import T``, the ``pdb`` imports, the
``torch.profiler`` import and the pip shim module; P2.3 removes the
``torch.profiler`` import and the pip shim module; P2.3 removed the
remaining ``distutils`` import. The list below is therefore the
*expected* state of the tree - a new finding fails this test.
"""
@@ -143,12 +147,9 @@ def test_real_tree_findings(scanner):
assert report["closure_file_count"] > 0
located = {(f["path"], f["line"], f["pattern"]) for f in findings}
expected = {
("modules/deps/basicsr/archs/arch_util.py", 6, "distutils"),
}
assert located == expected, (
"the vendored closure must only contain the open findings "
f"({sorted(located)}); P2.1/P2.3 must have removed the others"
assert located == set(), (
"the vendored closure must be free of compatibility findings, "
f"got {sorted(located)}"
)
@@ -201,8 +202,8 @@ def test_closure_contains_the_eagerly_scanned_modules(scanner):
@pytest.mark.unit
def test_exit_code_contract(scanner, monkeypatch):
"""Exit code 0 = no findings, 1 = findings found (a result, not a crash)."""
assert scanner.main(["--quiet"]) == 1, (
"the real tree has findings, so the scanner must exit 1"
assert scanner.main(["--quiet"]) == 0, (
"the real tree must be free of findings, so the scanner must exit 0"
)
monkeypatch.setattr(
scanner,
@@ -228,3 +229,65 @@ def test_audit_doc_lists_every_finding(scanner):
for finding in report["findings"]:
location = f"{finding['path']}:{finding['line']}"
assert location in text, f"{location} is not in the fix list"
# ---------------------------------------------------------------------------
# P2.3 - the mechanical Python 3.13 / numpy 2 fixes
# ---------------------------------------------------------------------------
#: Imports every module of the runtime closure in a **fresh** interpreter (the
#: import must not leak into the pytest session) and reports the failures.
_CLOSURE_IMPORT_SCRIPT = """\
import importlib, json, sys
sys.path.insert(0, sys.argv[1])
failures = []
for name in json.loads(sys.argv[2]):
try:
importlib.import_module(name)
except BaseException as exc:
failures.append({"module": name, "type": type(exc).__name__, "error": str(exc)})
print("KEEP_CLOSURE_FAILURES:" + json.dumps(failures))
"""
def _closure_module_names(report):
"""The importable module names of the vendored part of the closure."""
names = []
for rel in report["closure_files"]:
if not rel.startswith("modules/deps/"):
continue
dotted = rel[len("modules/deps/"):].replace("/", ".").removesuffix(".py")
if dotted.endswith(".__init__"):
dotted = dotted[: -len(".__init__")]
names.append(dotted)
return names
@pytest.mark.unit
def test_every_closure_module_imports(scanner):
"""P2.3 acceptance: every module of the runtime closure still imports.
The imports run in a subprocess: the vendored packages register their
classes in global registries and must not pollute the pytest session.
"""
report = scanner.scan_repo(REPO_ROOT)
names = _closure_module_names(report)
assert names, "the runtime closure is empty"
result = subprocess.run(
[sys.executable, "-c", _CLOSURE_IMPORT_SCRIPT, str(DEPS_DIR), json.dumps(names)],
cwd=str(REPO_ROOT),
capture_output=True,
text=True,
timeout=600,
)
sentinel = [
line for line in result.stdout.splitlines() if line.startswith("KEEP_CLOSURE_FAILURES:")
]
assert sentinel, (
f"the closure import probe produced no result\n"
f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}"
)
failures = json.loads(sentinel[-1].split(":", 1)[1])
assert failures == [], f"closure modules failed to import: {failures}"