-
Notifications
You must be signed in to change notification settings - Fork 1
Refactor options manager #39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| import logging | ||
|
|
||
|
|
||
| LOGGER = logging.getLogger("petsctools") | ||
|
|
||
| debug = LOGGER.debug | ||
| info = LOGGER.info | ||
| warning = LOGGER.warning | ||
| error = LOGGER.error | ||
| critical = LOGGER.critical |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,19 +3,19 @@ | |
| import weakref | ||
| import contextlib | ||
| import functools | ||
| import itertools | ||
| import warnings | ||
| from functools import cached_property | ||
| from typing import Any, Iterable | ||
|
|
||
| import petsc4py | ||
|
|
||
| import petsctools.log | ||
| from petsctools.appctx import AppContextManager | ||
| from petsctools.exceptions import ( | ||
| PetscToolsException, | ||
| PetscToolsWarning, | ||
| PetscToolsNotInitialisedException, | ||
| ) | ||
| from petsctools.appctx import AppContextManager | ||
|
|
||
|
|
||
| _commandline_options = None | ||
|
|
@@ -280,6 +280,7 @@ def get_default_options(default_options_set: DefaultOptionSet, | |
| return default_options | ||
|
|
||
|
|
||
| # TODO: Add note about how we 'freeze' options at instantiation | ||
| class OptionsManager: | ||
| """Class that helps with managing setting PETSc options. | ||
|
|
||
|
|
@@ -405,76 +406,72 @@ class OptionsManager: | |
| AppContextManager | ||
| """ | ||
|
|
||
| count = itertools.count() | ||
| count = 0 | ||
|
|
||
| def __init__(self, parameters: dict, | ||
| options_prefix: str | None = None, | ||
| default_prefix: str | None = None, | ||
| default_options_set: DefaultOptionSet | None = None, | ||
| appmngr: AppContextManager | None = None): | ||
| super().__init__() | ||
| if parameters is None: | ||
| parameters = {} | ||
| else: | ||
| # Convert nested dicts | ||
| parameters = flatten_parameters(parameters) | ||
|
|
||
| # If no prefix is provided generate a default prefix | ||
| # and ignore any command line options | ||
| if options_prefix is None: | ||
| default_prefix = default_prefix or "petsctools_" | ||
| default_prefix = _validate_prefix(default_prefix) | ||
| self.options_prefix = f"{default_prefix}{next(self.count)}_" | ||
| self.parameters = parameters | ||
| self.to_delete = set(parameters) | ||
|
|
||
| options_prefix = f"{default_prefix}{self.count}_" | ||
| self.count += 1 | ||
| unsafe_prefix = True | ||
| else: | ||
| options_prefix = _validate_prefix(options_prefix) | ||
| self.options_prefix = options_prefix | ||
|
|
||
| # Are we part of a solver set sharing defaults? | ||
| if default_options_set: | ||
| if options_prefix not in default_options_set.custom_prefixes: | ||
| raise ValueError( | ||
| f"The options_prefix {options_prefix} must be one" | ||
| f" of the custom_prefixes of the DefaultOptionSet" | ||
| f" {default_options_set.custom_prefixes}") | ||
| default_options = get_default_options( | ||
| default_options_set, self.options_object) | ||
| else: | ||
| default_options = {} | ||
|
|
||
| # Note: we need to know which parameters to_delete | ||
| # so we need to exclude the relevant command line | ||
| # options when combining the parameters from the | ||
| # defaults and the source code. | ||
|
|
||
| # Start building parameters from the defaults so | ||
| # that they will overwritten by any other source. | ||
| self.parameters = { | ||
| k: v | ||
| for k, v in default_options.items() | ||
| if options_prefix + k not in get_commandline_options() | ||
| } | ||
|
|
||
| # Update using the parameters passed in the code but | ||
| # exclude those options from the dict that were passed | ||
| # on the commandline because those have global scope and are | ||
| # not under the control of the options manager. | ||
| self.parameters.update({ | ||
| k: v | ||
| for k, v in parameters.items() | ||
| if options_prefix + k not in get_commandline_options() | ||
| }) | ||
| self.to_delete = set(self.parameters) | ||
|
|
||
| # Now update parameters from options, so that they're | ||
| # available to solver setup (for, e.g., matrix-free). | ||
| # Can't ask for the prefixed guy in the options object, | ||
| # 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 | ||
| unsafe_prefix = False | ||
|
|
||
| # Are we part of a solver set sharing defaults? | ||
| if default_options_set: | ||
| if options_prefix not in default_options_set.custom_prefixes: | ||
| raise ValueError( | ||
| f"The options_prefix {options_prefix} must be one" | ||
| f" of the custom_prefixes of the DefaultOptionSet" | ||
| f" {default_options_set.custom_prefixes}") | ||
| default_options = get_default_options( | ||
| default_options_set, self.options_object) | ||
| else: | ||
| default_options = {} | ||
|
|
||
| # Start building parameters from the defaults so | ||
| # that they will overwritten by any other source. | ||
| parameters = default_options | parameters | ||
|
|
||
| # The parameters to drop from the global options when we leave the | ||
| # inserted_options context. This is everything except for options | ||
| # passed on the command line. | ||
| to_delete = set(parameters.keys()) | ||
| warned = False | ||
| for full_key, v in self.options_object.getAll().items(): | ||
| if full_key.startswith(options_prefix): | ||
| key = full_key[len(options_prefix):] | ||
|
|
||
| if unsafe_prefix and not warned: | ||
| petsctools.log.warning( | ||
| "Setting options using an autogenerated prefix " | ||
| f"({options_prefix}) is unsafe" | ||
| ) | ||
| warned = True # only warn once | ||
|
Comment on lines
+458
to
+463
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we collect all of the unsafe options that were passed and raise a single warning at the end that lists all of them? |
||
|
|
||
| parameters[key] = v | ||
|
|
||
| if key in to_delete: | ||
| # option is set globally, don't drop when we exit the | ||
| # context manager | ||
| to_delete.remove(key) | ||
|
|
||
| self.parameters = parameters | ||
| self.to_delete = to_delete | ||
| self.options_prefix = options_prefix | ||
|
|
||
| self._setfromoptions = False | ||
|
|
||
|
|
@@ -559,15 +556,18 @@ def inserted_options(self): | |
| else: | ||
| yield | ||
| finally: | ||
| for k in self.to_delete: | ||
| for k in self.parameters: | ||
| if self.options_object.used(self.options_prefix + k): | ||
| self._used_options.add(k) | ||
| for k in self.to_delete: | ||
| del self.options_object[self.options_prefix + k] | ||
|
|
||
| @functools.cached_property | ||
| def options_object(self): | ||
| from petsc4py import PETSc | ||
|
|
||
| # We can't pass the prefix here because that doesn't DTRT | ||
| # for flag options | ||
| return PETSc.Options() | ||
|
|
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -157,3 +157,57 @@ def test_default_options(): | |||||||||
| assert options2.parameters["opt2"] == "2" | ||||||||||
| assert options2.parameters["opt3"] == "3" | ||||||||||
| assert options2.parameters["opt4"] == "6" | ||||||||||
|
|
||||||||||
|
|
||||||||||
| @pytest.mark.skipnopetsc4py | ||||||||||
| @pytest.mark.parametrize("options_prefix", (None, "", "custom_")) | ||||||||||
| def test_commandline_options(caplog, options_prefix): | ||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please can you add an error message to each assertion describing what failure means? |
||||||||||
| from petsc4py import PETSc | ||||||||||
|
|
||||||||||
| if options_prefix is None: | ||||||||||
| true_prefix = f"petsctools_{petsctools.OptionsManager.count}_" | ||||||||||
| else: | ||||||||||
| true_prefix = options_prefix | ||||||||||
|
|
||||||||||
| # Put some options in the database as though they were passed by a user on | ||||||||||
| # the command line | ||||||||||
| options = PETSc.Options() | ||||||||||
| options["opt1"] = "unused" | ||||||||||
| options[f"{true_prefix}opt2"] = "will_overwrite" | ||||||||||
| options[f"{true_prefix}opt3"] = "extra" | ||||||||||
|
|
||||||||||
| default_params = { | ||||||||||
| # this will get ignored because we pass something on the command line | ||||||||||
| "opt2": "default_opt2", | ||||||||||
| # this will be inserted and popped from the database | ||||||||||
| "opt4": "default_opt4", | ||||||||||
| } | ||||||||||
| om = petsctools.OptionsManager( | ||||||||||
| default_params, options_prefix=options_prefix | ||||||||||
| ) | ||||||||||
| assert om.options_prefix == true_prefix | ||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
|
|
||||||||||
| with om.inserted_options(): | ||||||||||
| assert options["opt1"] == "unused" | ||||||||||
| assert options[f"{om.options_prefix}opt2"] == "will_overwrite" | ||||||||||
| assert options[f"{om.options_prefix}opt3"] == "extra" | ||||||||||
| assert options[f"{om.options_prefix}opt4"] == "default_opt4" | ||||||||||
|
|
||||||||||
| if options_prefix is None: | ||||||||||
| assert len(caplog.records) == 1 | ||||||||||
| assert caplog.messages[0].startswith( | ||||||||||
| "Setting options using an autogenerated prefix" | ||||||||||
| ) | ||||||||||
| else: | ||||||||||
| assert not caplog.records | ||||||||||
|
|
||||||||||
| # make sure the command line options are persistent | ||||||||||
| assert options["opt1"] == "unused" | ||||||||||
| assert options[f"{om.options_prefix}opt2"] == "will_overwrite" | ||||||||||
| assert options[f"{om.options_prefix}opt3"] == "extra" | ||||||||||
| assert f"{om.options_prefix}opt4" not in options | ||||||||||
|
|
||||||||||
| # TODO | ||||||||||
| # make sure we warn on usage if prefix is None | ||||||||||
| # and the appctx too | ||||||||||
|
|
||||||||||
|
Comment on lines
+209
to
+213
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Your refactor using dict piping made a mistake I made with the default options sets obvious.
The
default_optionswere grabbed from the global options so we shouldn't delete these. For these options we even know for sure that they will be wanted elsewhere!