diff --git a/src/__init__.py b/src/__init__.py index 06a2b507b..8964fe9dc 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -13,6 +13,7 @@ import atexit import binascii import collections +import contextlib import glob import importlib.util import inspect @@ -30,7 +31,30 @@ import weakref import zipfile -from . import extra + +@contextlib.contextmanager +def _swig_import(): + ''' + Context manager for importing our SWIG modules `extra` and `mupdf`. + + With swig<4.4, module initialisation gives `DeprecationWarning: builtin + type SwigPyPacked has no __module__ attribute` (also for SwigPyObject and + swigvarlink). If warnings are errors (for example `python -W error` or + `pytest -W error`), swig's init code ignores the resulting exception and + then crashes with SEGV. So we ignore these warnings while importing. See + #4392 and https://github.com/swig/swig/issues/3061. + ''' + with warnings.catch_warnings(): + warnings.filterwarnings( + 'ignore', + 'builtin type .+ has no __module__ attribute', + DeprecationWarning, + ) + yield + + +with _swig_import(): + from . import extra # Set up g_out_log and g_out_message from environment variables. # @@ -396,10 +420,11 @@ def _warn_layout_once(): # directory for conventional wheel installs, or from separate place in case # we are using a separately-installed system installation of mupdf. # - try: - from . import mupdf - except Exception: - import mupdf + with _swig_import(): + try: + from . import mupdf + except Exception: + import mupdf if hasattr(mupdf, 'internal_check_ndebug'): mupdf.internal_check_ndebug() mupdf.reinit_singlethreaded() diff --git a/src/extra.i b/src/extra.i index a72f8c13f..b1f5f411a 100644 --- a/src/extra.i +++ b/src/extra.i @@ -13,6 +13,13 @@ otherwise compilation can fail because free() and malloc() are not declared. */ %init %{ + /* With swig<4.4, if we have not created swig's globals object by the time + we are unloaded at exit, swig creates it then, which gives `DeprecationWarning: + builtin type swigvarlink has no __module__ attribute` and a SEGV if warnings + are errors. So we create it now, while src/__init__.py is ignoring these + warnings. See #4392. */ + SWIG_globals(); + /* Initialise some globals that require Python functions. [Prior to 2023-08-18 we initialised these global variables inline, diff --git a/tests/test_general.py b/tests/test_general.py index 8d5a1892b..142f6e310 100644 --- a/tests/test_general.py +++ b/tests/test_general.py @@ -2037,7 +2037,22 @@ def test_4392(): e3 = subprocess.run(command, shell=1, check=0).returncode print(f'{e3=}') - print(f'{e1=} {e2=} {e3=}', flush=1) + # With swig<4.4 an unclosed file used to give a SEGV at exit, because + # swig created a new type when unloading `extra`. + command = f'{sys.executable} -Werror -c "import pymupdf; open({path!r})"' + print(f'Running: {command}', flush=1) + e4 = subprocess.run(command, shell=1, check=0).returncode + print(f'{e4=}') + + # Check that we don't get swig's `DeprecationWarning: builtin type + # SwigPyPacked has no __module__ attribute` etc. + command = f'{sys.executable} -W default -c "import pymupdf"' + print(f'Running: {command}', flush=1) + cp = subprocess.run(command, shell=1, check=0, capture_output=1, text=1) + print(f'{cp.returncode=}') + print(f'{cp.stderr=}') + + print(f'{e1=} {e2=} {e3=} {e4=}', flush=1) print(f'{pymupdf.swig_version=}', flush=1) print(f'{pymupdf.swig_version_tuple=}', flush=1) @@ -2052,30 +2067,20 @@ def test_4392(): # Exit code 4: pytest command line usage error # Exit code 5: No tests were collected + # With swig<4.4 we used to get SEGV's here, but now we ignore swig's + # warnings when importing. assert e1 == 5 - if pymupdf.swig_version_tuple >= (4, 4): - if sysconfig.get_config_var('Py_GIL_DISABLED') == 1 and sys._is_gil_enabled(): - assert e2 == 4 - else: - assert e2 == 5 - if sysconfig.get_config_var('Py_GIL_DISABLED') == 1 and sys._is_gil_enabled(): - # GIL warning results in failure because of -Werror. - assert e3 == 1 - else: - assert e3 == 0 + if sysconfig.get_config_var('Py_GIL_DISABLED') == 1 and sys._is_gil_enabled(): + # GIL warning results in failure because of -Werror. + assert e2 == 4 + assert e3 == 1 + assert e4 == 1 else: - # We get SEGV's etc with older swig. - if platform.system() == 'Windows': - assert (e2, e3) == (0xc0000005, 0xc0000005) - elif platform.system() == 'Linux': - # On plain linux we get (139, 139). On manylinux we get (-11, - # -11). On MacOS we get (-11, -11). - assert (e2, e3) == (139, 139) or (e2, e3) == (-11, -11) - elif platform.system() == 'Darwin': - # python3.14t gives (4, -11)? - assert (e2, e3) == (-11, -11) or (e2, e3) == (4, -11) - else: - assert e2 and e3 + assert e2 == 5 + assert e3 == 0 + assert e4 == 0 + assert cp.returncode == 0 + assert 'has no __module__ attribute' not in cp.stderr def test_4639():