From 43816c2528697d0428bab1b9432d681684d6ed6f Mon Sep 17 00:00:00 2001 From: Pablo Brubeck Date: Thu, 27 Aug 2026 11:12:38 +0100 Subject: [PATCH] Leave PETSc initialisable after importing petsctools `options.py` needs `PETSc.Options` at import time, to subclass it, but must not initialise PETSc to get it, so it calls `petsc4py.lib.ImportPETSc`. That registers the extension module under the name of the `petsc4py/PETSc.py` shim it shadows, both in `sys.modules` and as an attribute of the `petsc4py` package. The shim is the only thing that ever calls `PETSc._initialize`, so once it is shadowed it never runs again: every later `from petsc4py import PETSc` in the process hands back a PETSc that nothing has initialised. Anything that then reaches into PETSc dereferences a null pointer. In Firedrake's `tests/tsfc` session, which imports `petsctools` through `finat.citations` and never calls `petsctools.init`, every worker segfaults at its first form compilation: petsctools/citation.py:58 in cite -> PETSc.Sys.registerCitation libpetsc.so.3.025 PetscSegBufferGet+0x1c <- PetscCitationsList is NULL Put the import machinery back as it was found, so that the shim still runs for whoever imports `petsc4py.PETSc` next. The module object is unchanged: CPython keeps its own cache of single-phase extension modules, so the later import returns the same object and only adds the initialisation. This was found with the assistance of Claude Code. Co-Authored-By: Claude Opus 5 --- petsctools/options.py | 34 +++++++++++++++++++++++++++++++++- tests/test_options.py | 20 ++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/petsctools/options.py b/petsctools/options.py index 5e382eb..f4a073d 100644 --- a/petsctools/options.py +++ b/petsctools/options.py @@ -4,6 +4,7 @@ import functools import itertools import numbers +import sys import types import warnings import weakref @@ -20,9 +21,40 @@ PetscToolsWarning, ) + +def _import_petsc_without_initialising(): + """Import ``petsc4py.PETSc`` without initialising PETSc. + + Returns + ------- + types.ModuleType + The ``petsc4py.PETSc`` extension module. + + Notes + ----- + This is the dance `petsctools.init` does, except that it puts the import + machinery back as it found it. `petsc4py.lib.ImportPETSc` registers the + extension module as ``petsc4py.PETSc``, which is the name of the shim it + shadows, and that shim is the only thing that ever calls + ``PETSc._initialize``. Left registered, it stops the shim running for the + rest of the process, so a later ``from petsc4py import PETSc`` hands back + a PETSc that nothing has initialised, and the first call into it + dereferences a null pointer. + + """ + in_modules = "petsc4py.PETSc" in sys.modules + is_attribute = hasattr(petsc4py, "PETSc") + PETSc = petsc4py.lib.ImportPETSc() + if not in_modules: + del sys.modules["petsc4py.PETSc"] + if not is_attribute: + delattr(petsc4py, "PETSc") + return PETSc + + # Do this instead of 'from petsc4py import PETSc' to make sure we don't import # (and hence initialise) PETSc. -PETSc = petsc4py.lib.ImportPETSc() +PETSc = _import_petsc_without_initialising() _commandline_options = None diff --git a/tests/test_options.py b/tests/test_options.py index 1fa3b0a..db334b0 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -1,3 +1,5 @@ +import subprocess +import sys import warnings import pytest @@ -376,3 +378,21 @@ def test_inserted_options_dict(): with petsctools.inserted_options(parameters=params, options_prefix=prefix): assert PETSc.Options().getInt("prefix_opt_int") == 3 assert PETSc.Options().getBool("prefix_opt_flag") + + +@pytest.mark.skipnopetsc4py +def test_importing_petsctools_leaves_petsc_initialisable(): + """Importing petsctools must not stop the petsc4py shim from running. + + The shim is the only thing that calls ``PETSc._initialize``, so for any + code that does not call `petsctools.init` itself it is what initialises + PETSc. Run in a subprocess, since PETSc is already initialised here. + """ + script = ( + "import sys\n" + "import petsctools\n" + "assert 'petsc4py.PETSc' not in sys.modules\n" + "from petsc4py import PETSc\n" + "assert PETSc.Sys.isInitialized()\n" + ) + subprocess.run([sys.executable, "-c", script], check=True)