From cdff71c9b55dca4741184e489fa5d72dbcb8eba3 Mon Sep 17 00:00:00 2001 From: Anthony Shoumikhin Date: Mon, 26 Oct 2026 11:03:42 -0700 Subject: [PATCH] Stop installing test-only packages with the executorch package (#24143) ### Summary `pip install executorch` installs four packages that are only there for ExecuTorch's own tests: `expecttest`, `hypothesis`, `kgb` and `parameterized`. `hypothesis` also brings `sortedcontainers`. So every user installs five packages they never need. This moves the four from the package's runtime dependencies (`_base_dependencies()` in `setup.py`) to `requirements-dev.txt`. The developer setup and every CI job that runs tests already install that file, so tests still get them. Nothing a user imports needs them. Three shipped files do, but only tests or a code generator use them: `backends/cadence/aot/typing_stubs.py`, `examples/models/llama/source_transformation/test_attention_sink.py`, and `exir/dialects/edge/spec/gen.py`. Effect, resolving the declared dependencies for Python 3.12 (without `torch`): 55 packages before, 50 after on macOS arm64 and Linux x86_64. The same five go away on Linux aarch64 and Windows. Kept on purpose: `coremltools` and `scikit-learn` (the Core ML backend needs both), and `hydra-core`, `omegaconf`, `pandas`, `tabulate` and `pytorch-tokenizers`, which shipped modules import. The first commit fixes the wheel's clean-install check. It hid undeclared packages so that `importlib.util.find_spec` raised instead of returning `None`. `torch._dynamo` probes for `hypothesis` with `find_spec` when it loads, so the check failed once `hypothesis` was no longer declared, although a real install works. ### Test plan On macOS arm64, in a fresh environment with only the trimmed dependencies, `torch` 2.14.0 and a nightly `executorch` installed with `--no-deps`: - Tried to import all ~3,000 modules in the package. Only the three files above failed for lack of a removed package. The rest imported, or stopped on unrelated optional packages such as vendor SDKs. - Exported a small model with XNNPACK and with Core ML, ran both, and matched eager PyTorch. - The clean-install check fails before the first commit and passes after it, and still fails when a checked module imports an undeclared package. Tests that use the moved packages pass with `requirements-dev.txt` installed. (cherry picked from commit ff09af0d68187dd5fe5bc4a611d105ff357bbcbd) --- .ci/scripts/wheel/test_clean_install.py | 36 ++++++++++++++----------- requirements-dev.txt | 5 ++++ setup.py | 4 --- 3 files changed, 25 insertions(+), 20 deletions(-) diff --git a/.ci/scripts/wheel/test_clean_install.py b/.ci/scripts/wheel/test_clean_install.py index 65943277324..13840ed8807 100644 --- a/.ci/scripts/wheel/test_clean_install.py +++ b/.ci/scripts/wheel/test_clean_install.py @@ -215,10 +215,10 @@ def run_tests(work_dir: Path) -> None: # Reads its arguments from stdin, blocks the named modules, then imports. A blocked module -# raises ModuleNotFoundError exactly as it would be absent, so the traceback shows the import -# chain that wanted it. +# behaves like a missing one to import and find_spec, so the traceback shows the import chain +# that wanted it. _IMPORT_PROBE = """ -import importlib.abc, importlib.util, json, os, sys +import importlib, json, os, sys # Drop the working directory, which `python -c` puts first. The equivalent flag, -P, is only # available from 3.11. @@ -229,19 +229,23 @@ def run_tests(work_dir: Path) -> None: request = json.load(sys.stdin) blocked = set(request["blocked"]) - -class _Blocker(importlib.abc.MetaPathFinder): - def find_spec(self, fullname, path=None, target=None): - if fullname.split(".")[0] in blocked: - raise ModuleNotFoundError( - f"No module named {fullname!r} (not declared by the executorch wheel)", - name=fullname, - ) - return None - - -sys.meta_path.insert(0, _Blocker()) -importlib.import_module(request["module"]) +# A None entry in sys.modules makes an import raise ModuleNotFoundError while +# importlib.util.find_spec returns None, which is how a missing package behaves. A finder that +# raises from find_spec instead breaks code that probes for optional packages, such as +# torch._dynamo, which looks up hypothesis that way. +for name in blocked: + sys.modules[name] = None + +try: + importlib.import_module(request["module"]) +except ModuleNotFoundError as error: + package = (error.name or "").split(".")[0] + if package in blocked: + raise ModuleNotFoundError( + f"No module named {package!r} (not declared by the executorch wheel)", + name=package, + ) from error + raise """ diff --git a/requirements-dev.txt b/requirements-dev.txt index e9575084648..a6f33b6f885 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -21,3 +21,8 @@ pytest-xdist pytest-rerunfailures==15.1 pytest-json-report gguf # For extension/llm/export/test/test_gguf.py (GGUF Q4_K/Q6_K dequant tests). +# Needed only by tests and development tools, so the executorch package does not depend on them. +expecttest +hypothesis +kgb +parameterized diff --git a/setup.py b/setup.py index ec9ba0aa4ea..e5fefa74bc1 100644 --- a/setup.py +++ b/setup.py @@ -1154,15 +1154,11 @@ def _base_dependencies() -> List[str]: project's runtime needs. """ return [ - "expecttest", "flatbuffers", - "hypothesis", - "kgb", "mpmath==1.3.0", "numpy>=2.0.0; python_version >= '3.10'", "packaging", "pandas>=2.2.2; python_version >= '3.10'", - "parameterized", # The Qualcomm backend needs both, on different paths. Any lowering reads the CPU vendor # to disable an mkldnn path that crashes on AMD, whether or not an SDK is already set up. # Fetching an SDK additionally needs requests, which its downloader imports at module