From 5b387844b09fd9ee31bc5e628509bc1df2cabc1a Mon Sep 17 00:00:00 2001 From: Connor Ward Date: Mon, 15 Jun 2026 12:45:22 +0100 Subject: [PATCH 1/2] Warn if PETSc is already initialised inside petsctools.init. This functionality was rejected from petsc4py and so should live here. It is unfortunately complicated due to the petsc4py init process. Also tidies up some docstrings and moves some exceptions to exceptions.py which is now the established convention. --- .github/workflows/core.yml | 20 ++++++++---- petsctools/__init__.py | 16 ++++------ petsctools/config.py | 6 +--- petsctools/exceptions.py | 12 +++++++ petsctools/init.py | 65 ++++++++++++++++++++++++++------------ 5 files changed, 78 insertions(+), 41 deletions(-) diff --git a/.github/workflows/core.yml b/.github/workflows/core.yml index 6b2fc9a..7ba65f7 100644 --- a/.github/workflows/core.yml +++ b/.github/workflows/core.yml @@ -113,18 +113,24 @@ jobs: . venv-petsctools/bin/activate pip install --verbose $PETSC_DIR/src/binding/petsc4py - # Make sure that running 'import petsctools' does not initialise PETSc - # (e.g. by running 'from petsc4py import PETSc'). Otherwise 'petsctools.init' - # does nothing. We check this by passing a PETSc option on the command line - # and making sure that it is processed. - - name: Test lazy PETSc initialisation + - name: Test PETSc initialisation if: success() || steps.install-petsc4py.conclusion == 'success' run: | . venv-petsctools/bin/activate + + : # Make sure that running 'import petsctools' does not initialise PETSc + : # (e.g. by running 'from petsc4py import PETSc'). Otherwise 'petsctools.init' + : # does nothing. We check this by passing a PETSc option on the command line + : # and making sure that it is processed. if [ -z "$( python -c 'import petsctools; petsctools.init()' -log_view | grep 'PETSc Performance Summary' )" ]; then + echo "ERROR: '-log_view' flag not passed to PETSc" + exit 1 + fi + + : # Test that running 'petsctools.init' after PETSc is initialised raises a warning + if [ -z "$( python -c 'import petsc4py, petsctools; petsc4py.init(); petsctools.init()' | grep 'PETSc has already been initialised' )" ]; then + echo "ERROR: petsctools.init should have raised a warning" exit 1 - else - exit 0 fi - name: Run tests with petsc4py diff --git a/petsctools/__init__.py b/petsctools/__init__.py index 88df751..ba5cf4e 100644 --- a/petsctools/__init__.py +++ b/petsctools/__init__.py @@ -1,5 +1,4 @@ from .config import ( # noqa: F401 - MissingPetscException, get_config, get_petsc_dir, get_petsc_arch, @@ -8,7 +7,12 @@ get_petscconf_h, get_external_packages, ) -from .exceptions import PetscToolsException # noqa: F401 +from .exceptions import ( # noqa: F401 + PetscToolsException, + MissingPetscException, + InvalidEnvironmentException, + InvalidPetscVersionException, +) from .utils import PETSC4PY_INSTALLED # Now conditionally import the functions that depend on petsc4py. If petsc4py @@ -26,11 +30,7 @@ print_citations_at_exit, ) from .config import get_blas_library # noqa: F401 - from .init import ( # noqa: F401 - InvalidEnvironmentException, - InvalidPetscVersionException, - init, - ) + from .init import init # noqa: F401 from .options import ( # noqa: F401 flatten_parameters, get_commandline_options, @@ -54,8 +54,6 @@ def __getattr__(name): "cite", "print_citations_at_exit", "get_blas_library", - "InvalidEnvironmentException", - "InvalidPetscVersionException", "init", "flatten_parameters", "get_commandline_options", diff --git a/petsctools/config.py b/petsctools/config.py index 909634c..71c5d2c 100644 --- a/petsctools/config.py +++ b/petsctools/config.py @@ -2,11 +2,7 @@ import os import subprocess -from petsctools.exceptions import PetscToolsException - - -class MissingPetscException(PetscToolsException): - pass +from petsctools.exceptions import MissingPetscException def get_config(): diff --git a/petsctools/exceptions.py b/petsctools/exceptions.py index 5d35e4c..0708773 100644 --- a/petsctools/exceptions.py +++ b/petsctools/exceptions.py @@ -10,5 +10,17 @@ class PetscToolsAppctxException(PetscToolsException): """Exception raised when the Appctx is missing an entry.""" +class InvalidEnvironmentException(PetscToolsException): + pass + + +class InvalidPetscVersionException(PetscToolsException): + pass + + +class MissingPetscException(PetscToolsException): + pass + + class PetscToolsWarning(UserWarning): """Generic base class for petsctools warnings.""" diff --git a/petsctools/init.py b/petsctools/init.py index 10720ed..d5151bf 100644 --- a/petsctools/init.py +++ b/petsctools/init.py @@ -1,37 +1,64 @@ import os import sys +import types import warnings +from collections.abc import Sequence from pathlib import Path +import petsc4py +import petsc4py.lib from packaging.specifiers import SpecifierSet from packaging.version import Version import petsctools.options -from petsctools.exceptions import PetscToolsException - - -class InvalidEnvironmentException(PetscToolsException): - pass - - -class InvalidPetscVersionException(PetscToolsException): - pass - - -def init(argv=None, *, version_spec=""): - """Initialise PETSc.""" - import petsc4py - +from petsctools.exceptions import ( + InvalidEnvironmentException, InvalidPetscVersionException +) + + +def init( + argv: Sequence[str] | None = None, + *, + version_spec: SpecifierSet | str = "", +) -> types.ModuleType: + """Initialise PETSc. + + Parameters + ---------- + argv + Command line options to be passed to PETSc at initialisation. If + unspecified then `sys.argv` is used. + version_spec + String describing PETSc version constraints. For example + '>=3.25.2,<3.26'. + + Returns + ------- + types.ModuleType + The `petsc4py.PETSc` module. This is convenient for avoiding + boilerplate. + + """ if argv is None: argv = sys.argv - petsc4py.init(argv) + # We have to do this dance because we need to access petsc4py.PETSc without + # initialising PETSc. This is what happens in + # https://gitlab.com/petsc/petsc/-/blob/main/src/binding/petsc4py/src/petsc4py/PETSc.py + PETSc = petsc4py.lib.ImportPETSc() + if PETSc.Sys.isInitialized(): + warnings.warn( + "Calling petsctools.init but PETSc has already been initialised, " + "any command line options will be ignored.", + stacklevel=2, + ) + else: + PETSc._initialize(argv) + check_environment_matches_petsc4py_config() check_petsc_version(version_spec) # Save the command line options so they may be inspected later - from petsc4py import PETSc - petsctools.options._commandline_options = frozenset( PETSc.Options().getAll() ) @@ -40,8 +67,6 @@ def init(argv=None, *, version_spec=""): def check_environment_matches_petsc4py_config(): - import petsc4py - config = petsc4py.get_config() petsc_dir = config["PETSC_DIR"] petsc_arch = config["PETSC_ARCH"] From a9e5af9198fbd74dcf574f2ec66ca9ddeadc7a55 Mon Sep 17 00:00:00 2001 From: Connor Ward Date: Mon, 15 Jun 2026 13:15:57 +0100 Subject: [PATCH 2/2] fixup --- .github/workflows/core.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/core.yml b/.github/workflows/core.yml index 7ba65f7..1619d4b 100644 --- a/.github/workflows/core.yml +++ b/.github/workflows/core.yml @@ -128,7 +128,7 @@ jobs: fi : # Test that running 'petsctools.init' after PETSc is initialised raises a warning - if [ -z "$( python -c 'import petsc4py, petsctools; petsc4py.init(); petsctools.init()' | grep 'PETSc has already been initialised' )" ]; then + if [ -z "$( 2>&1 python -c 'import petsc4py, petsctools; petsc4py.init(); petsctools.init()' | grep 'PETSc has already been initialised' )" ]; then echo "ERROR: petsctools.init should have raised a warning" exit 1 fi