huggingface/transformers · #48992

Serve legacy module aliases through a sys.meta_path finder

Rocketknight1 · merged Oct 16, 20263 files · 127 + / 52 −
src/transformers/__init__.py27 + / 51 −
@@ -20,9 +20,8 @@  __version__ = "5.18.0.dev0" -import importlib+import re import sys-import types from pathlib import Path from typing import TYPE_CHECKING @@ -54,7 +53,7 @@ from .utils import is_torchvision_available as is_torchvision_available from .utils import is_vision_available as is_vision_available from .utils import logging as logging-from .utils.import_utils import define_import_structure+from .utils.import_utils import _LegacyModuleAliasFinder, define_import_structure   logger = logging.get_logger(__name__)  # pylint: disable=invalid-name@@ -808,7 +807,16 @@     from .utils.quantization_config import VptqConfig as VptqConfig     from .video_processing_utils import BaseVideoProcessor as BaseVideoProcessor else:+    # Deprecated top-level module paths -> replacements. Declared here so the legacy names are also part of the lazy+    # import structure, keeping `transformers.tokenization_utils_fast` attribute access working.+    _LEGACY_MODULE_ALIASES = {+        "tokenization_utils_fast": "tokenization_utils_tokenizers",+        "tokenization_utils": "tokenization_utils_sentencepiece",+        "image_processing_utils_fast": "image_processing_backends",+    }     _import_structure = {k: set(v) for k, v in _import_structure.items()}+    for _alias in _LEGACY_MODULE_ALIASES:+        _import_structure.setdefault(_alias, set())      import_structure = define_import_structure(Path(__file__).parent / "models", prefix="models")     import_structure[frozenset({})].update(_import_structure)@@ -821,54 +829,22 @@         extra_objects={"__version__": __version__},     ) -    def _create_module_alias(alias: str, target: str) -> None:-        """-        Lazily redirect legacy module paths to their replacements without importing heavy deps.-        """-        module = types.ModuleType(alias)-        module.__doc__ = f"Alias module for backward compatibility with `{target}`."-        # Set __file__ explicitly so that inspect.py's hasattr(module, '__file__') check-        # never falls through to __getattr__ and triggers a premature (possibly circular) import.-        module.__file__ = None--        def _get_target():-            return importlib.import_module(target, __name__)--        module.__getattr__ = lambda name: getattr(_get_target(), name)-        module.__dir__ = lambda: dir(_get_target())--        sys.modules[alias] = module-        setattr(sys.modules[__name__], alias.rsplit(".", 1)[-1], module)--    _create_module_alias(f"{__name__}.tokenization_utils_fast", ".tokenization_utils_tokenizers")-    _create_module_alias(f"{__name__}.tokenization_utils", ".tokenization_utils_sentencepiece")-    _create_module_alias(f"{__name__}.image_processing_utils_fast", ".image_processing_backends")--    for _proc_file in sorted((Path(__file__).parent / "models").rglob("image_processing_*.py")):-        _model = _proc_file.parent.name-        _module = _proc_file.stem-        _target = f".models.{_model}.{_module}"-        _create_module_alias(f"{__name__}.models.{_model}.{_module}_fast", _target)--        # Also map XImageProcessorFast -> XImageProcessor for backward compat with old class names.-        def getattr_factory(target):-            def _getattr(name):-                if name.endswith("Fast"):-                    new_name = name.removesuffix("Fast")-                    logger.warning_once(-                        "Accessing `%s` from `%s`. Returning `%s` instead. Behavior may be "-                        "different and this alias will be removed in future versions.",-                        name,-                        target,-                        new_name,-                    )-                    return getattr(importlib.import_module(target, __name__), new_name)-                # Silently forward non-Fast names to target (transparent alias behavior)-                return getattr(importlib.import_module(target, __name__), name)--            return _getattr--        sys.modules[f"{__name__}.models.{_model}.{_module}_fast"].__getattr__ = getattr_factory(_target)+    # Deprecated module paths, served on demand by `_LegacyModuleAliasFinder`. `models/x/image_processing_x_fast.py`+    # became `models/x/image_processing_x.py` when torchvision became the default backend (PIL: `image_processing_pil_x.py`).+    _LEGACY_FAST_IMAGE_PROCESSOR_MODULE = re.compile(+        rf"{re.escape(__name__)}\.models\.(\w+)\.image_processing_(?!pil_)(\w+)_fast"+    )++    def _resolve_legacy_module_alias(fullname: str) -> str | None:+        if (match := _LEGACY_FAST_IMAGE_PROCESSOR_MODULE.fullmatch(fullname)) is not None:+            return f"{__name__}.models.{match[1]}.image_processing_{match[2]}"+        package, _, name = fullname.rpartition(".")+        if package == __name__ and name in _LEGACY_MODULE_ALIASES:+            return f"{__name__}.{_LEGACY_MODULE_ALIASES[name]}"+        return None++    if not any(isinstance(finder, _LegacyModuleAliasFinder) for finder in sys.meta_path):  # this init can run twice+        sys.meta_path.append(_LegacyModuleAliasFinder(_resolve_legacy_module_alias))  if not is_torch_available():     logger.warning_advice(
src/transformers/utils/import_utils.py33 + / 0 −
@@ -16,6 +16,7 @@ """  import functools+import importlib.abc import importlib.machinery import importlib.metadata import importlib.util@@ -2802,6 +2803,38 @@ class OptionalDependencyNotAvailable(BaseException):     """Internally used error class for signalling an optional dependency was not found."""  +class _LegacyModuleAliasFinder(importlib.abc.MetaPathFinder, importlib.abc.Loader):+    """+    Serves deprecated module paths as copies of their replacement, created only when actually imported. Nothing is+    pre-registered in `sys.modules`, so tools that scan it and probe attributes (unittest's `assertWarns`, `pickle`,+    `inspect`, ...) never trigger imports of optional heavy dependencies.+    """++    def __init__(self, resolve: Callable[[str], str | None]):+        self.resolve = resolve  # legacy module name -> replacement module name, `None` if not an alias++    def find_spec(self, fullname, path=None, target=None):+        replacement = self.resolve(fullname)+        if replacement is None or importlib.util.find_spec(replacement) is None:+            return None+        return importlib.machinery.ModuleSpec(fullname, self, loader_state=replacement)++    def exec_module(self, module):+        # Import the replacement first: a missing optional dependency fails right here, at the user's import statement.+        replacement = importlib.import_module(module.__spec__.loader_state)+        vars(module).update({k: v for k, v in vars(replacement).items() if k == "__all__" or not k.startswith("__")})++        def __getattr__(name):  # `XImageProcessorFast` classes were renamed `XImageProcessor`+            if name.endswith("ImageProcessorFast") and hasattr(replacement, name[:-4]):+                logger.warning_once(+                    f"`{name}` is deprecated, use `{name[:-4]}` from `{replacement.__name__}` instead."+                )+                return getattr(replacement, name[:-4])+            raise AttributeError(f"module {module.__name__!r} has no attribute {name!r}")++        module.__getattr__ = __getattr__++ def direct_transformers_import(path: str, file="__init__.py") -> ModuleType:     """Imports transformers directly 
tests/utils/test_import_utils.py67 + / 1 −
@@ -3,11 +3,18 @@ from types import ModuleType from unittest.mock import DEFAULT, MagicMock, patch +import pytest from packaging.version import parse as parse_version from parameterized import parameterized  from transformers import logging-from transformers.testing_utils import CaptureLogger, LoggingLevel, require_torch, run_test_using_subprocess+from transformers.testing_utils import (+    CaptureLogger,+    LoggingLevel,+    require_torch,+    require_torchvision,+    run_test_using_subprocess,+) from transformers.utils.import_utils import (     _candidate_distribution_names,     _is_package_available,@@ -419,3 +426,62 @@ def run(x):         return x + 1 if helper(*args) else x - 1      run(torch.zeros(3))  # a graph break inside the helper would raise here+++@run_test_using_subprocess+def test_legacy_module_aliases_are_served_on_demand():+    """+    Deprecated module paths (`transformers.tokenization_utils_fast`, `transformers.models.x.image_processing_x_fast`,+    ...) are served by a `sys.meta_path` finder and must not be pre-registered in `sys.modules`: tools that scan+    `sys.modules` and probe attributes (unittest's `assertWarns`, `pickle`, `inspect`) would otherwise trigger imports+    of optional heavy dependencies. Regression test for https://github.com/huggingface/transformers/issues/48966.+    """+    import unittest+    import warnings++    import transformers+    from transformers.utils import import_utils++    finders = [f for f in sys.meta_path if isinstance(f, import_utils._LegacyModuleAliasFinder)]+    assert len(finders) == 1+    finder = finders[0]+    pre_registered = [name for name in sys.modules if finder.resolve(name) is not None]+    assert pre_registered == [], f"legacy module paths pre-registered in sys.modules: {pre_registered}"+    assert not any(name.startswith("image_processing_") for name in vars(transformers))++    # Scanning `sys.modules` (what `assertWarns` does on entry) must not import anything.+    modules_before = len(sys.modules)+    with unittest.TestCase().assertWarns(UserWarning):+        warnings.warn("probe", UserWarning)+    assert len(sys.modules) == modules_before++    # Legacy paths still work, as thin shims over the replacement module.+    from transformers.tokenization_utils_fast import PreTrainedTokenizerFast++    assert PreTrainedTokenizerFast is transformers.PreTrainedTokenizerFast+    shim = sys.modules["transformers.tokenization_utils_fast"]+    assert shim is transformers.tokenization_utils_fast+    assert shim.__spec__.loader is finder+    assert "PreTrainedTokenizerFast" in vars(shim)+    # Module metadata is the shim's own, so attribute probes behave like on any other module.+    assert getattr(shim, "__warningregistry__", None) is None+    assert getattr(shim, "does_not_exist", None) is None++    with pytest.raises(ModuleNotFoundError):+        import transformers.models.aria.image_processing_nope_fast  # noqa: F401+++@require_torchvision+def test_legacy_fast_image_processor_module_alias():+    """`models/x/image_processing_x_fast.py` became `models/x/image_processing_x.py`; the old path is an alias."""+    import transformers.models.aria.image_processing_aria_fast as legacy_module+    from transformers.models.aria.image_processing_aria import AriaImageProcessor+    from transformers.models.aria.image_processing_aria_fast import AriaImageProcessorFast  # legacy class name++    assert AriaImageProcessorFast is AriaImageProcessor+    assert legacy_module.AriaImageProcessor is AriaImageProcessor+    assert legacy_module.__all__ == ["AriaImageProcessor"]++    # Only paths that actually existed are aliased.+    with pytest.raises(ModuleNotFoundError):+        import transformers.models.aria.image_processing_pil_aria_fast  # noqa: F401