diff --git a/.circleci/config.yml b/.circleci/config.yml index 7a7f4aaf7c1b..4e84c85d932d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -175,27 +175,34 @@ jobs: - store_artifacts: path: ~/transformers/installed.txt - run: make check-repository-consistency - - run: - name: "Test import with all backends (torch + PIL + torchvision)" - command: python -c "from transformers import *" || (echo '🚨 import failed with all backends. Fix unprotected imports!! 🚨'; exit 1) - - run: - name: "Test import with torch only (no PIL, no torchvision)" - command: | - uv pip uninstall Pillow torchvision -q - python -c "from transformers import *" || (echo '🚨 import failed with torch only (no PIL). Fix unprotected imports!! 🚨'; exit 1) - uv pip install -e ".[quality]" -q - - run: - name: "Test import with PIL only (no torch, no torchvision)" - command: | - uv pip uninstall torch torchvision torchaudio -q - python -c "from transformers import *" || (echo '🚨 import failed with PIL only (no torch). Fix unprotected imports!! 🚨'; exit 1) - uv pip install -e ".[quality]" -q - - run: - name: "Test import with torch + PIL, no torchvision" - command: | - uv pip uninstall torchvision -q - python -c "from transformers import *" || (echo '🚨 import failed with torch+PIL but no torchvision. Fix unprotected imports!! 🚨'; exit 1) - uv pip install -e ".[quality]" -q + + check_imports: + working_directory: ~/transformers + docker: + - image: huggingface/transformers-consistency + resource_class: large + environment: + TRANSFORMERS_IS_CI: yes + parallelism: 1 + steps: + - checkout + - run: apt-get update && apt-get install -y make + - run: uv pip install -e ".[quality]" + - run: make check-imports + + check_modular: + working_directory: ~/transformers + docker: + - image: huggingface/transformers-consistency + resource_class: large + environment: + TRANSFORMERS_IS_CI: yes + parallelism: 1 + steps: + - checkout + - run: apt-get update && apt-get install -y make + - run: uv pip install -e ".[quality]" + - run: make check-modular workflows: @@ -209,6 +216,8 @@ workflows: - check_circleci_user - check_code_quality - check_repository_consistency + - check_imports + - check_modular - fetch_tests setup_and_quality_2: @@ -219,6 +228,8 @@ workflows: - check_circleci_user - check_code_quality - check_repository_consistency + - check_imports + - check_modular - fetch_tests: # [reference] https://circleci.com/docs/contexts/ context: @@ -230,4 +241,6 @@ workflows: - check_circleci_user - check_code_quality - check_repository_consistency + - check_imports + - check_modular - fetch_all_tests diff --git a/Makefile b/Makefile index faebf4cbdfc3..57fab430def3 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # make sure to test the local checkout in scripts and not the pre-installed one (don't use quotes!) export PYTHONPATH = src -.PHONY: style typing check-code-quality check-repository-consistency check-repo fix-repo test test-examples benchmark codex claude clean-ai +.PHONY: style typing check-code-quality check-imports check-modular check-repository-consistency check-repo fix-repo test test-examples benchmark codex claude clean-ai # Runs all linting/formatting scripts, most notably ruff @@ -29,12 +29,18 @@ check-code-quality: init_isort,\ auto_mappings -# Runs a full repository consistency check. +# Runs import tests under various backend combinations (split out for parallel CI). +check-imports: + @python utils/checkers.py imports + +# Runs modular file conversion check (split out for parallel CI). +check-modular: + @python utils/checkers.py modular_conversion + +# Runs a full repository consistency check (without imports and modular conversion). check-repository-consistency: @python utils/checkers.py \ - imports,\ copies,\ - modular_conversion,\ doc_toc,\ docstrings,\ dummies,\ diff --git a/utils/checkers.py b/utils/checkers.py index dfef9aea285e..9316c9ca1a66 100644 --- a/utils/checkers.py +++ b/utils/checkers.py @@ -29,7 +29,9 @@ import shutil import subprocess import sys +import tempfile import threading +import time from collections import deque from pathlib import Path @@ -195,12 +197,51 @@ def run_deps_table_checker(fix=False, line_callback=None): return 0, output +# Each scenario: (label, extra packages to install alongside transformers) +_IMPORT_SCENARIOS = [ + ("all backends (torch + PIL + torchvision)", ["torch", "Pillow", "torchvision"]), + ("torch only (no PIL, no torchvision)", ["torch"]), + ("PIL only (no torch, no torchvision)", ["Pillow"]), + ("torch + PIL, no torchvision", ["torch", "Pillow"]), +] + + def run_imports_checker(fix=False, line_callback=None): - """Check that all public imports work.""" - rc, output = _run_cmd([sys.executable, "-c", "from transformers import *"], line_callback=line_callback) - if rc != 0: - return rc, output + "Import failed, this means you introduced unprotected imports!\n" - return 0, output + """Check that public imports work under various backend combinations. + + Each scenario runs in an isolated temporary venv so the caller's + environment is never modified. + """ + all_output: list[str] = [] + + for label, extras in _IMPORT_SCENARIOS: + with tempfile.TemporaryDirectory() as tmpdir: + venv_dir = os.path.join(tmpdir, ".venv") + + # Create venv + rc, out = _run_cmd(["uv", "venv", venv_dir, "--python", sys.executable, "-q"], line_callback=line_callback) + all_output.append(out) + if rc != 0: + all_output.append(f"Failed to create venv for scenario: {label}\n") + return rc, "".join(all_output) + + # Install transformers (editable) + the scenario-specific packages + install_cmd = ["uv", "pip", "install", "-e", str(REPO_ROOT), *extras, "-q", "--python", venv_dir] + rc, out = _run_cmd(install_cmd, line_callback=line_callback) + all_output.append(out) + if rc != 0: + all_output.append(f"Failed to install packages for scenario: {label}\n") + return rc, "".join(all_output) + + # Run the import test inside the venv + venv_python = os.path.join(venv_dir, "bin", "python") + rc, out = _run_cmd([venv_python, "-c", "from transformers import *"], line_callback=line_callback) + all_output.append(out) + if rc != 0: + all_output.append(f"🚨 Import failed with {label}. Fix unprotected imports!! 🚨\n") + return rc, "".join(all_output) + + return 0, "".join(all_output) RUFF_TARGETS = ["examples", "tests", "src", "utils", "scripts", "benchmark", "benchmark_v2", "setup.py", "conftest.py"] @@ -314,6 +355,7 @@ def main(): is_tty = sys.stdout.isatty() and not is_ci failures = [] + total_t0 = time.monotonic() for name in names: label = CHECKERS[name][0] cmd_str = get_checker_command(name, fix=args.fix) @@ -322,9 +364,11 @@ def main(): window = SlidingWindow(label, max_lines=10) if cmd_str: window.add_line(f"$ {cmd_str}") + t0 = time.monotonic() rc, output = run_checker(name, fix=args.fix, line_callback=window.add_line) + elapsed = time.monotonic() - t0 window.finish(success=(rc == 0)) - print() + print(f" ({elapsed:.1f}s)") if rc != 0: failures.append(name) if not args.keep_going: @@ -333,23 +377,27 @@ def main(): print(f"{label}") if cmd_str: print(f"$ {cmd_str}") + t0 = time.monotonic() rc, output = run_checker(name, fix=args.fix) + elapsed = time.monotonic() - t0 tail = output.splitlines()[-10:] if tail: print("\n".join(tail)) status = "OK" if rc == 0 else "FAILED" - print(status) + print(f"{status} ({elapsed:.1f}s)") print() if rc != 0: failures.append(name) if not args.keep_going: sys.exit(1) + total_elapsed = time.monotonic() - total_t0 + if failures: - print(f"\n{len(failures)} failed: {', '.join(failures)}") + print(f"\n{len(failures)} failed: {', '.join(failures)} (total: {total_elapsed:.1f}s)") sys.exit(1) - print(f"\nAll {len(names)} checks passed.") + print(f"\nAll {len(names)} checks passed. (total: {total_elapsed:.1f}s)") if __name__ == "__main__":