Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion petsctools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@
# is not available then attempting to access these attributes will raise an
# informative error.
if PETSC4PY_INSTALLED:
from .citation import add_citation, cite, print_citations_at_exit # noqa: F401
from .citation import ( # noqa: F401
add_citation,
cite,
print_citations_at_exit,
)
from .config import get_blas_library # noqa: F401
from .init import ( # noqa: F401
InvalidEnvironmentException,
Expand Down
48 changes: 45 additions & 3 deletions petsctools/options.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from __future__ import annotations

import weakref
import contextlib
import functools
import itertools
import warnings
from typing import Any
from typing import Any, Iterable

import petsc4py

Expand Down Expand Up @@ -90,12 +91,42 @@ def munge(keys):
if option in new:
warnings.warn(
f"Ignoring duplicate option: {option} (existing value "
f"{new[option]}, new value {value})",
f"{new[option]}, new value {value})", PetscToolsWarning
)
new[option] = value
return new


def _warn_unused_options(all_options: Iterable, used_options: Iterable,
options_prefix: str = ""):
"""
Raise warnings for PETSc options which were not used.

This is meant only as a weakref.finalize callback for the OptionsManager.

Parameters
----------
all_options :
The full set of options passed to the OptionsManager.
used_options :
The options which were used during the OptionsManager's lifetime.
options_prefix :
The options_prefix of the OptionsManager.

Raises
------
PetscToolsWarning :
For every entry in all_options which is not in used_options.
"""
unused_options = set(all_options) - set(used_options)

for option in sorted(unused_options):
warnings.warn(
f"Unused PETSc option: {options_prefix+option}",
PetscToolsWarning
)


class OptionsManager:
"""Class that helps with managing setting PETSc options.

Expand Down Expand Up @@ -238,8 +269,17 @@ def __init__(self, parameters: dict, options_prefix: str | None):
# since that does not DTRT for flag options.
for k, v in self.options_object.getAll().items():
if k.startswith(self.options_prefix):
self.parameters[k[len(self.options_prefix) :]] = v
self.parameters[k[len(self.options_prefix):]] = v
self._setfromoptions = False
# Keep track of options used between invocations of inserted_options().
self._used_options = set()

# Decide whether to warn for unused options
with self.inserted_options():
if self.options_object.getInt("options_left", 0) > 0:
weakref.finalize(self, _warn_unused_options,
self.to_delete, self._used_options,
options_prefix=self.options_prefix)

def set_default_parameter(self, key: str, val: Any) -> None:
"""Set a default parameter value.
Expand Down Expand Up @@ -292,6 +332,8 @@ def inserted_options(self):
yield
finally:
for k in self.to_delete:
if self.options_object.used(self.options_prefix + k):
self._used_options.add(k)
del self.options_object[self.options_prefix + k]

@functools.cached_property
Expand Down
76 changes: 76 additions & 0 deletions tests/test_options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import warnings
import pytest
import petsctools


@pytest.fixture(autouse=True, scope="module")
def temporarily_remove_options():
"""Remove all options when the module is entered and reinsert them at exit.
This ensures that options in e.g. petscrc files will not pollute the tests.
"""
if petsctools.PETSC4PY_INSTALLED:
PETSc = petsctools.init()
options = PETSc.Options()
previous_options = {
k: v for k, v in options.getAll().items()
}
options.clear()
yield
if petsctools.PETSC4PY_INSTALLED:
for k, v in previous_options.items():
options[k] = v


@pytest.fixture(autouse=True)
def clear_options():
"""Clear any options from the database at the end of each test.
"""
yield
# PETSc already initialised by module scope fixture
from petsc4py import PETSc
PETSc.Options().clear()


@pytest.mark.skipnopetsc4py
@pytest.mark.parametrize("options_left", (-1, 0, 1),
ids=("no_options_left",
"options_left=0",
"options_left=1"))
def test_unused_options(options_left):
"""Check that unused solver options result in a warning in the log."""
# PETSc already initialised by module scope fixture
from petsc4py import PETSc

if options_left >= 0:
PETSc.Options()["options_left"] = options_left

parameters = {
"used": 1,
"not_used": 2,
}
options = petsctools.OptionsManager(parameters, options_prefix="optobj")

with options.inserted_options():
_ = PETSc.Options().getInt(options.options_prefix + "used")

# No warnings should be raised in this case.
if options_left <= 0:
with warnings.catch_warnings():
warnings.simplefilter("error")
del options
return

# Destroying the object will trigger the unused options warning
with pytest.warns() as records:
del options

# Exactly one option is both unused and not ignored
assert len(records) == 1
message = str(records[0].message)

# Does the warning include the options prefix?
assert "optobj" in message

# Do we only raise a warning for the unused option?
assert "optobj_not_used" in message
assert "optobj_used" not in message