diff --git a/petsctools/options.py b/petsctools/options.py index a7695f2..fe61931 100644 --- a/petsctools/options.py +++ b/petsctools/options.py @@ -1,5 +1,6 @@ from __future__ import annotations +import builtins import contextlib import functools import itertools @@ -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. @@ -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 @@ -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 @@ -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. + +""" + + _global_appctx_data = {} """The global storage for user data with arbitrary python types.""" @@ -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. @@ -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: @@ -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( diff --git a/tests/test_options.py b/tests/test_options.py index 64f03d9..4f4f1be 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -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