Skip to content

Commit 7cd5807

Browse files
committed
gh-154904: Speed up import of shutil by probing compression extensions
shutil imported bz2, lzma and compression.zstd at module scope only to set the _*_SUPPORTED flags, then discarded the bindings. Executing those pure Python wrappers also pulled in compression._common[._streams] and compression.zstd._zstdfile, a cost paid by every program that imports shutil even if it never touches an archive. Their only importable dependency that may be missing is the extension module each one wraps, so probe _bz2, _lzma and _zstd directly instead. This keeps the exact ImportError semantics of the previous code -- unlike a find_spec() check, which resolves the always-present wrapper and would report success on builds where the extension fails to load. The wrappers are still imported by tarfile/zipfile when an archive is actually created or extracted. Cuts the net cost of "import shutil" by ~2.5 ms (-19.6%) and of "import urllib.request" by ~2.3 ms (-6.4%).
1 parent 49f9667 commit 7cd5807

3 files changed

Lines changed: 51 additions & 6 deletions

File tree

Lib/shutil.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,28 @@
1818
except ImportError:
1919
_ZLIB_SUPPORTED = False
2020

21+
# bz2, lzma and compression.zstd are pure Python wrappers whose only
22+
# importable dependency that may be missing is the extension module they
23+
# wrap. Probe those extensions directly instead: it gives the same answer
24+
# without executing the wrappers, which shutil only needs when an archive
25+
# is actually created or extracted.
2126
try:
22-
import bz2
23-
del bz2
27+
import _bz2
28+
del _bz2
2429
_BZ2_SUPPORTED = True
2530
except ImportError:
2631
_BZ2_SUPPORTED = False
2732

2833
try:
29-
import lzma
30-
del lzma
34+
import _lzma
35+
del _lzma
3136
_LZMA_SUPPORTED = True
3237
except ImportError:
3338
_LZMA_SUPPORTED = False
3439

3540
try:
36-
from compression import zstd
37-
del zstd
41+
import _zstd
42+
del _zstd
3843
_ZSTD_SUPPORTED = True
3944
except ImportError:
4045
_ZSTD_SUPPORTED = False

Lib/test/test_shutil.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import os.path
1111
import errno
1212
import functools
13+
import importlib
1314
import socket
1415
import subprocess
1516
import random
@@ -32,6 +33,7 @@
3233
from test import support
3334
from test.support import os_helper, socket_helper
3435
from test.support.os_helper import TESTFN, FakePath
36+
from test.support.script_helper import assert_python_ok
3537

3638
TESTFN2 = TESTFN + "2"
3739
TESTFN_SRC = TESTFN + "_SRC"
@@ -2321,6 +2323,40 @@ def _boo(filename, extract_dir, extra):
23212323
unregister_unpack_format('Boo2')
23222324
self.assertEqual(get_unpack_formats(), formats)
23232325

2326+
def test_compression_supported_flags(self):
2327+
# shutil determines compression support by probing the extension
2328+
# modules (_bz2, _lzma, _zstd) rather than importing the pure Python
2329+
# wrappers. The answer must match what importing the wrapper does,
2330+
# including on builds where the extension is missing or fails to load.
2331+
for wrapper, supported in (
2332+
('zlib', shutil._ZLIB_SUPPORTED),
2333+
('bz2', shutil._BZ2_SUPPORTED),
2334+
('lzma', shutil._LZMA_SUPPORTED),
2335+
('compression.zstd', shutil._ZSTD_SUPPORTED),
2336+
):
2337+
with self.subTest(wrapper=wrapper):
2338+
try:
2339+
importlib.import_module(wrapper)
2340+
except ImportError:
2341+
importable = False
2342+
else:
2343+
importable = True
2344+
self.assertEqual(supported, importable)
2345+
2346+
def test_compression_wrappers_not_imported_by_shutil(self):
2347+
# Importing shutil must not pull in the compression wrappers: they are
2348+
# only needed once an archive is actually created or extracted, and
2349+
# importing them measurably slows down every process that uses shutil.
2350+
wrappers = ('bz2', 'lzma', 'compression', 'compression.zstd')
2351+
script = (
2352+
'import sys, shutil; '
2353+
f'print([m for m in {wrappers!r} if m in sys.modules])'
2354+
)
2355+
# -I so that a sitecustomize/usercustomize importing one of these
2356+
# cannot make the test fail spuriously.
2357+
rc, stdout, stderr = assert_python_ok('-I', '-c', script)
2358+
self.assertEqual(stdout.decode().strip(), '[]', stderr)
2359+
23242360

23252361
class TestMisc(BaseTest, unittest.TestCase):
23262362

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Speed up :mod:`shutil` import by probing the ``_bz2``, ``_lzma`` and
2+
``_zstd`` extension modules instead of importing the :mod:`bz2`,
3+
:mod:`lzma` and :mod:`compression.zstd` wrappers, which are now only
4+
imported when an archive is actually created or extracted.

0 commit comments

Comments
 (0)