Skip to content

Commit 3ca0eb1

Browse files
hmaarrfkseberg
authored andcommitted
MAINT: Lazy import testing on python >=3.7 (#14097)
On new python versions, the module level `__getattr__` can be used to import testing and Tester only when needed (at no speed cost, except for the first time import). Since most users never use testing, this avoids an expensive import.
1 parent b6a3ab5 commit 3ca0eb1

File tree

2 files changed

+54
-3
lines changed

2 files changed

+54
-3
lines changed

numpy/__init__.py

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,8 @@
166166
# now that numpy modules are imported, can initialize limits
167167
core.getlimits._register_known_types()
168168

169+
__all__.extend(['bool', 'int', 'float', 'complex', 'object', 'unicode',
170+
'str'])
169171
__all__.extend(['__version__', 'show_config'])
170172
__all__.extend(core.__all__)
171173
__all__.extend(_mat.__all__)
@@ -182,9 +184,35 @@
182184
oldnumeric = 'removed'
183185
numarray = 'removed'
184186

185-
# We don't actually use this ourselves anymore, but I'm not 100% sure that
186-
# no-one else in the world is using it (though I hope not)
187-
from .testing import Tester
187+
if sys.version_info[:2] >= (3, 7):
188+
# Importing Tester requires importing all of UnitTest which is not a
189+
# cheap import Since it is mainly used in test suits, we lazy import it
190+
# here to save on the order of 10 ms of import time for most users
191+
#
192+
# The previous way Tester was imported also had a side effect of adding
193+
# the full `numpy.testing` namespace
194+
#
195+
# module level getattr is only supported in 3.7 onwards
196+
# https://www.python.org/dev/peps/pep-0562/
197+
def __getattr__(attr):
198+
if attr == 'testing':
199+
import numpy.testing as testing
200+
return testing
201+
elif attr == 'Tester':
202+
from .testing import Tester
203+
return Tester
204+
else:
205+
raise AttributeError(
206+
"module %s has no attribute $s".format(__name__, attr))
207+
208+
209+
def __dir__():
210+
return __all__ + ['Tester', 'testing']
211+
212+
else:
213+
# We don't actually use this ourselves anymore, but I'm not 100% sure that
214+
# no-one else in the world is using it (though I hope not)
215+
from .testing import Tester
188216

189217
# Pytest testing
190218
from numpy._pytesttester import PytestTester

numpy/tests/test_public_api.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import division, absolute_import, print_function
22

33
import sys
4+
import subprocess
45

56
import numpy as np
67
import pytest
@@ -69,6 +70,28 @@ def test_numpy_namespace():
6970
assert bad_results == whitelist
7071

7172

73+
@pytest.mark.parametrize('name', ['testing', 'Tester'])
74+
def test_import_lazy_import(name):
75+
"""Make sure we can actually the the modules we lazy load.
76+
77+
While not exported as part of the public API, it was accessible. With the
78+
use of __getattr__ and __dir__, this isn't always true It can happen that
79+
an infinite recursion may happen.
80+
81+
This is the only way I found that would force the failure to appear on the
82+
badly implemented code.
83+
84+
We also test for the presence of the lazily imported modules in dir
85+
86+
"""
87+
exe = (sys.executable, '-c', "import numpy; numpy." + name)
88+
result = subprocess.check_output(exe)
89+
assert not result
90+
91+
# Make sure they are still in the __dir__
92+
assert name in dir(np)
93+
94+
7295
def test_numpy_linalg():
7396
bad_results = check_dir(np.linalg)
7497
assert bad_results == {}

0 commit comments

Comments
 (0)