diff --git a/.github/workflows/core.yml b/.github/workflows/core.yml index 6b2fc9a..1619d4b 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 "$( 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 - 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"]