Skip to content

Commit 76e49a7

Browse files
Raise a warning for unused petsc options in OptionsManager. (#21)
--------- Co-authored-by: Connor Ward <c.ward20@imperial.ac.uk>
1 parent 616d3d0 commit 76e49a7

3 files changed

Lines changed: 126 additions & 4 deletions

File tree

petsctools/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@
1414
# is not available then attempting to access these attributes will raise an
1515
# informative error.
1616
if PETSC4PY_INSTALLED:
17-
from .citation import add_citation, cite, print_citations_at_exit # noqa: F401
17+
from .citation import ( # noqa: F401
18+
add_citation,
19+
cite,
20+
print_citations_at_exit,
21+
)
1822
from .config import get_blas_library # noqa: F401
1923
from .init import ( # noqa: F401
2024
InvalidEnvironmentException,

petsctools/options.py

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
from __future__ import annotations
22

3+
import weakref
34
import contextlib
45
import functools
56
import itertools
67
import warnings
7-
from typing import Any
8+
from typing import Any, Iterable
89

910
import petsc4py
1011

@@ -90,12 +91,42 @@ def munge(keys):
9091
if option in new:
9192
warnings.warn(
9293
f"Ignoring duplicate option: {option} (existing value "
93-
f"{new[option]}, new value {value})",
94+
f"{new[option]}, new value {value})", PetscToolsWarning
9495
)
9596
new[option] = value
9697
return new
9798

9899

100+
def _warn_unused_options(all_options: Iterable, used_options: Iterable,
101+
options_prefix: str = ""):
102+
"""
103+
Raise warnings for PETSc options which were not used.
104+
105+
This is meant only as a weakref.finalize callback for the OptionsManager.
106+
107+
Parameters
108+
----------
109+
all_options :
110+
The full set of options passed to the OptionsManager.
111+
used_options :
112+
The options which were used during the OptionsManager's lifetime.
113+
options_prefix :
114+
The options_prefix of the OptionsManager.
115+
116+
Raises
117+
------
118+
PetscToolsWarning :
119+
For every entry in all_options which is not in used_options.
120+
"""
121+
unused_options = set(all_options) - set(used_options)
122+
123+
for option in sorted(unused_options):
124+
warnings.warn(
125+
f"Unused PETSc option: {options_prefix+option}",
126+
PetscToolsWarning
127+
)
128+
129+
99130
class OptionsManager:
100131
"""Class that helps with managing setting PETSc options.
101132
@@ -238,8 +269,17 @@ def __init__(self, parameters: dict, options_prefix: str | None):
238269
# since that does not DTRT for flag options.
239270
for k, v in self.options_object.getAll().items():
240271
if k.startswith(self.options_prefix):
241-
self.parameters[k[len(self.options_prefix) :]] = v
272+
self.parameters[k[len(self.options_prefix):]] = v
242273
self._setfromoptions = False
274+
# Keep track of options used between invocations of inserted_options().
275+
self._used_options = set()
276+
277+
# Decide whether to warn for unused options
278+
with self.inserted_options():
279+
if self.options_object.getInt("options_left", 0) > 0:
280+
weakref.finalize(self, _warn_unused_options,
281+
self.to_delete, self._used_options,
282+
options_prefix=self.options_prefix)
243283

244284
def set_default_parameter(self, key: str, val: Any) -> None:
245285
"""Set a default parameter value.
@@ -292,6 +332,8 @@ def inserted_options(self):
292332
yield
293333
finally:
294334
for k in self.to_delete:
335+
if self.options_object.used(self.options_prefix + k):
336+
self._used_options.add(k)
295337
del self.options_object[self.options_prefix + k]
296338

297339
@functools.cached_property

tests/test_options.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import warnings
2+
import pytest
3+
import petsctools
4+
5+
6+
@pytest.fixture(autouse=True, scope="module")
7+
def temporarily_remove_options():
8+
"""Remove all options when the module is entered and reinsert them at exit.
9+
This ensures that options in e.g. petscrc files will not pollute the tests.
10+
"""
11+
if petsctools.PETSC4PY_INSTALLED:
12+
PETSc = petsctools.init()
13+
options = PETSc.Options()
14+
previous_options = {
15+
k: v for k, v in options.getAll().items()
16+
}
17+
options.clear()
18+
yield
19+
if petsctools.PETSC4PY_INSTALLED:
20+
for k, v in previous_options.items():
21+
options[k] = v
22+
23+
24+
@pytest.fixture(autouse=True)
25+
def clear_options():
26+
"""Clear any options from the database at the end of each test.
27+
"""
28+
yield
29+
# PETSc already initialised by module scope fixture
30+
from petsc4py import PETSc
31+
PETSc.Options().clear()
32+
33+
34+
@pytest.mark.skipnopetsc4py
35+
@pytest.mark.parametrize("options_left", (-1, 0, 1),
36+
ids=("no_options_left",
37+
"options_left=0",
38+
"options_left=1"))
39+
def test_unused_options(options_left):
40+
"""Check that unused solver options result in a warning in the log."""
41+
# PETSc already initialised by module scope fixture
42+
from petsc4py import PETSc
43+
44+
if options_left >= 0:
45+
PETSc.Options()["options_left"] = options_left
46+
47+
parameters = {
48+
"used": 1,
49+
"not_used": 2,
50+
}
51+
options = petsctools.OptionsManager(parameters, options_prefix="optobj")
52+
53+
with options.inserted_options():
54+
_ = PETSc.Options().getInt(options.options_prefix + "used")
55+
56+
# No warnings should be raised in this case.
57+
if options_left <= 0:
58+
with warnings.catch_warnings():
59+
warnings.simplefilter("error")
60+
del options
61+
return
62+
63+
# Destroying the object will trigger the unused options warning
64+
with pytest.warns() as records:
65+
del options
66+
67+
# Exactly one option is both unused and not ignored
68+
assert len(records) == 1
69+
message = str(records[0].message)
70+
71+
# Does the warning include the options prefix?
72+
assert "optobj" in message
73+
74+
# Do we only raise a warning for the unused option?
75+
assert "optobj_not_used" in message
76+
assert "optobj_used" not in message

0 commit comments

Comments
 (0)