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
57 changes: 49 additions & 8 deletions petsctools/options.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import builtins
import contextlib
import functools
import itertools
Expand Down Expand Up @@ -274,8 +275,10 @@ def custom_prefixes(self):
for ending in self.custom_prefix_endings)


def get_default_options(default_options_set: DefaultOptionSet,
options: petsc4py.PETSc.Options | None = None) -> dict:
def get_default_options(
default_options_set: DefaultOptionSet,
options: Options | petsc4py.PETSc.Options | None = None,
) -> dict:
"""
Extract default options for subsolvers with similar prefixes.

Expand All @@ -284,7 +287,7 @@ def get_default_options(default_options_set: DefaultOptionSet,
default_options_set
The :class:`DefaultOptionSet` which defines the shared options.
options
The ``PETSc.Options`` database to use. If not provided then the global
The options database to use. If not provided then the global
database will be used.

Returns
Expand All @@ -296,8 +299,7 @@ def get_default_options(default_options_set: DefaultOptionSet,
DefaultOptionSet
"""
if options is None:
from petsc4py import PETSc
options = PETSc.Options()
options = Options()

base_prefix = default_options_set.base_prefix
custom_prefixes = default_options_set.custom_prefixes
Expand Down Expand Up @@ -622,6 +624,14 @@ def options_object(self):
return PETSc.Options()


_option_types = {}
"""Mapping from options names to the type of the stored value.

This allows us to cast the retrieved object back to the right type.

Comment thread
JHopeCollins marked this conversation as resolved.
"""


_global_appctx_data = {}
"""The global storage for user data with arbitrary python types."""

Expand Down Expand Up @@ -1084,12 +1094,37 @@ def __getitem__(self, option: str | AppContextKey, /) -> Any:
If the ``Options`` does not contain a value for ``option``.
"""
# might raise a KeyError, which we want
value = super().__getitem__(option)
if isinstance(value, str) and value.startswith(_APPCTX_KEY_PREFIX):
value: str = super().__getitem__(option)

if value.startswith(_APPCTX_KEY_PREFIX):
return _global_appctx_data[value]
else:

# A native type, try to perform a cast
try:
opt_type = _option_types[f"{self.prefix or ''}{option}"]
except KeyError:
# Option was not inserted using petsctools.Options, can't
# do anything more
return value

match opt_type:
case builtins.str:
pass
case builtins.int:
value = int(value)
case builtins.float:
value = float(value)
case builtins.bool:
if value == "true":
value = True
else:
assert value == "false"
value = False
case types.NoneType:
assert value == ""
value = None
return value

def __setitem__(self, option: str | AppContextKey, value: Any, /) -> None:
"""Insert an option into the database.

Expand All @@ -1107,6 +1142,10 @@ def __setitem__(self, option: str | AppContextKey, value: Any, /) -> None:
value_id = AppContextKey._generate_key()
_global_appctx_data[value_id] = value
value = value_id

# Save the type of value so we can cast to it in __getitem__
_option_types[f"{self.prefix or ''}{option}"] = type(value)

super().__setitem__(option, value)

def __delitem__(self, option: str | AppContextKey, /) -> None:
Expand All @@ -1130,6 +1169,8 @@ def __delitem__(self, option: str | AppContextKey, /) -> None:
if not isinstance(value, _native_petsc_option_types):
value_id = super().__getitem__(option)
del _global_appctx_data[value_id]

del _option_types[f"{self.prefix or ''}{option}"]
super().__delitem__(option)

def get(
Expand Down
42 changes: 42 additions & 0 deletions tests/test_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,3 +412,45 @@ def test_importing_petsctools_leaves_petsc_initialisable():
"assert PETSc.Sys.isInitialized()\n"
)
subprocess.run([sys.executable, "-c", script], check=True)


@pytest.mark.skipnopetsc4py
def test_options_preserve_types():
opts = petsctools.Options()

items = [666, "a string", 1.234, 1.234e11, True, False, None]
for item in items:
opts["my_option"] = item
assert opts["my_option"] == item
assert type(opts["my_option"]) is type(item)

del opts["my_option"]
assert len(petsctools.options._option_types) == 0


@pytest.mark.skipnopetsc4py
def test_options_missing_types():
# Test that options inserted using PETSc.Options instead of
# petsctools.Options still work even though we don't know the type
from petsc4py import PETSc

petsc_opts = PETSc.Options()
petsctools_opts = petsctools.Options()

items = [
(666, petsc_opts.getInt),
("a string", petsc_opts.getString),
(1.234, petsc_opts.getReal),
(1.234e11, petsc_opts.getReal),
(True, petsc_opts.getBool),
(False, petsc_opts.getBool),
(None, None), # no suitable default getter for None
]
for item, getter in items:
petsc_opts["my_option"] = item

assert isinstance(petsc_opts["my_option"], str)
assert isinstance(petsctools_opts["my_option"], str)

if getter is not None:
assert getter("my_option") == item
Loading