|
3 | 3 | """ |
4 | 4 |
|
5 | 5 | import importlib |
| 6 | +import importlib.machinery |
6 | 7 | import os |
7 | 8 | import sys |
8 | 9 | import tempfile |
|
59 | 60 | #: is listed here. |
60 | 61 | STUBBED_MODULES: set[str] = set() |
61 | 62 |
|
| 63 | +# Probes that should return False on the stub (so call sites like |
| 64 | +# `torch.cuda.is_available()` take the "no GPU" / "not available" branch |
| 65 | +# instead of seeing a truthy MagicMock and crashing later on a `>` comparison). |
| 66 | +_STUB_FALSE_PROBES = frozenset( |
| 67 | + {"is_available", "is_initialized", "is_built"} |
| 68 | +) |
| 69 | + |
| 70 | + |
| 71 | +def _make_stub_class(name: str): |
| 72 | + """Build a stub class whose instantiation returns a fresh MagicMock. |
| 73 | +
|
| 74 | + The kwargs to `stub_cls(...)` are set as attributes on the returned mock, |
| 75 | + so `VADIterator(model, threshold=0.5, min_silence_duration_ms=1500)` gives |
| 76 | + back a mock you can read `.threshold` / `.min_silence_duration_ms` from. |
| 77 | + Each call returns a NEW mock — critical for tests that compare two |
| 78 | + separately-constructed objects (e.g. `VadEndpointer._iterator` vs |
| 79 | + `_soft_iterator`); MagicMock's `return_value` reuse would alias them and |
| 80 | + make per-iterator updates clobber each other. |
| 81 | +
|
| 82 | + Names in `_STUB_FALSE_PROBES` return `False` instead of a mock, so probe |
| 83 | + calls like `torch.cuda.is_available()` are falsy and call sites skip the |
| 84 | + GPU/load branch. |
| 85 | +
|
| 86 | + A metaclass on the returned class routes `cls.SubName` through |
| 87 | + `_make_stub_class` again, so chains like `torch.cuda.is_available()` |
| 88 | + resolve (each link is a fresh stub class; the `is_available` one returns |
| 89 | + False per the probe list). |
| 90 | + """ |
| 91 | + |
| 92 | + def __new__(cls, *args, **kwargs): |
| 93 | + if name in _STUB_FALSE_PROBES: |
| 94 | + return False |
| 95 | + m = MagicMock() |
| 96 | + for k, v in kwargs.items(): |
| 97 | + setattr(m, k, v) |
| 98 | + return m |
| 99 | + |
| 100 | + return _StubClassMeta(name, (), {"__new__": __new__}) |
| 101 | + |
| 102 | + |
| 103 | +class _StubClassMeta(type): |
| 104 | + def __getattr__(cls, name): |
| 105 | + return _make_stub_class(name) |
| 106 | + |
62 | 107 |
|
63 | 108 | class _StubModule(types.ModuleType): |
64 | | - """A module whose every attribute access returns a fresh MagicMock, so |
65 | | - `import x`, `from x import Y`, and `x.Y(...)` all succeed harmlessly.""" |
| 109 | + """Stand-in for a heavy module that isn't installed (CI without GPU stack). |
| 110 | +
|
| 111 | + `import x` returns this; `from x import Y` resolves Y via `__getattr__` |
| 112 | + to a stub class (see `_make_stub_class`). `x.Y(...)` instantiates that |
| 113 | + class and returns a fresh MagicMock with kwargs as attributes. |
| 114 | + """ |
66 | 115 |
|
67 | 116 | def __getattr__(self, name): # noqa: D401 - simple delegation |
68 | | - return MagicMock(name=f"{self.__name__}.{name}") |
| 117 | + # `__path__` is iterated by the import machinery when treating a |
| 118 | + # module as a package; returning a non-iterable stub class here |
| 119 | + # would break transitively-imported real packages that touch torch |
| 120 | + # (e.g. thinc via spacy via misaki). An empty list is what the |
| 121 | + # machinery would default to anyway. |
| 122 | + if name == "__path__": |
| 123 | + return [] |
| 124 | + return _make_stub_class(name) |
69 | 125 |
|
70 | 126 |
|
71 | 127 | for _name in _HEAVY_MODULES: |
72 | 128 | try: |
73 | 129 | importlib.import_module(_name) |
74 | 130 | except Exception: |
75 | | - sys.modules[_name] = _StubModule(_name) |
| 131 | + stub = _StubModule(_name) |
| 132 | + # Set a real __spec__ — the default ModuleType has __spec__ = None |
| 133 | + # which makes importlib.util.find_spec raise ValueError and breaks |
| 134 | + # tests that probe the dep via find_spec (e.g. test_cpu_imports). |
| 135 | + stub.__spec__ = importlib.machinery.ModuleSpec(name=_name, loader=None) |
| 136 | + sys.modules[_name] = stub |
76 | 137 | STUBBED_MODULES.add(_name) |
77 | 138 |
|
78 | 139 |
|
|
0 commit comments