387 lines
13 KiB
Python
387 lines
13 KiB
Python
![]() |
# Copyright 2017 The Abseil Authors.
|
||
|
#
|
||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
# you may not use this file except in compliance with the License.
|
||
|
# You may obtain a copy of the License at
|
||
|
#
|
||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||
|
#
|
||
|
# Unless required by applicable law or agreed to in writing, software
|
||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
# See the License for the specific language governing permissions and
|
||
|
# limitations under the License.
|
||
|
|
||
|
"""Decorator and context manager for saving and restoring flag values.
|
||
|
|
||
|
There are many ways to save and restore. Always use the most convenient method
|
||
|
for a given use case.
|
||
|
|
||
|
Here are examples of each method. They all call ``do_stuff()`` while
|
||
|
``FLAGS.someflag`` is temporarily set to ``'foo'``::
|
||
|
|
||
|
from absl.testing import flagsaver
|
||
|
|
||
|
# Use a decorator which can optionally override flags via arguments.
|
||
|
@flagsaver.flagsaver(someflag='foo')
|
||
|
def some_func():
|
||
|
do_stuff()
|
||
|
|
||
|
# Use a decorator which can optionally override flags with flagholders.
|
||
|
@flagsaver.flagsaver((module.FOO_FLAG, 'foo'), (other_mod.BAR_FLAG, 23))
|
||
|
def some_func():
|
||
|
do_stuff()
|
||
|
|
||
|
# Use a decorator which does not override flags itself.
|
||
|
@flagsaver.flagsaver
|
||
|
def some_func():
|
||
|
FLAGS.someflag = 'foo'
|
||
|
do_stuff()
|
||
|
|
||
|
# Use a context manager which can optionally override flags via arguments.
|
||
|
with flagsaver.flagsaver(someflag='foo'):
|
||
|
do_stuff()
|
||
|
|
||
|
# Save and restore the flag values yourself.
|
||
|
saved_flag_values = flagsaver.save_flag_values()
|
||
|
try:
|
||
|
FLAGS.someflag = 'foo'
|
||
|
do_stuff()
|
||
|
finally:
|
||
|
flagsaver.restore_flag_values(saved_flag_values)
|
||
|
|
||
|
# Use the parsing version to emulate users providing the flags.
|
||
|
# Note that all flags must be provided as strings (unparsed).
|
||
|
@flagsaver.as_parsed(some_int_flag='123')
|
||
|
def some_func():
|
||
|
# Because the flag was parsed it is considered "present".
|
||
|
assert FLAGS.some_int_flag.present
|
||
|
do_stuff()
|
||
|
|
||
|
# flagsaver.as_parsed() can also be used as a context manager just like
|
||
|
# flagsaver.flagsaver()
|
||
|
with flagsaver.as_parsed(some_int_flag='123'):
|
||
|
do_stuff()
|
||
|
|
||
|
# The flagsaver.as_parsed() interface also supports FlagHolder objects.
|
||
|
@flagsaver.as_parsed((module.FOO_FLAG, 'foo'), (other_mod.BAR_FLAG, '23'))
|
||
|
def some_func():
|
||
|
do_stuff()
|
||
|
|
||
|
# Using as_parsed with a multi_X flag requires a sequence of strings.
|
||
|
@flagsaver.as_parsed(some_multi_int_flag=['123', '456'])
|
||
|
def some_func():
|
||
|
assert FLAGS.some_multi_int_flag.present
|
||
|
do_stuff()
|
||
|
|
||
|
# If a flag name includes non-identifier characters it can be specified like
|
||
|
# so:
|
||
|
@flagsaver.as_parsed(**{'i-like-dashes': 'true'})
|
||
|
def some_func():
|
||
|
do_stuff()
|
||
|
|
||
|
We save and restore a shallow copy of each Flag object's ``__dict__`` attribute.
|
||
|
This preserves all attributes of the flag, such as whether or not it was
|
||
|
overridden from its default value.
|
||
|
|
||
|
WARNING: Currently a flag that is saved and then deleted cannot be restored. An
|
||
|
exception will be raised. However if you *add* a flag after saving flag values,
|
||
|
and then restore flag values, the added flag will be deleted with no errors.
|
||
|
"""
|
||
|
|
||
|
import collections
|
||
|
import functools
|
||
|
import inspect
|
||
|
from typing import overload, Any, Callable, Mapping, Tuple, TypeVar, Type, Sequence, Union
|
||
|
|
||
|
from absl import flags
|
||
|
|
||
|
FLAGS = flags.FLAGS
|
||
|
|
||
|
|
||
|
# The type of pre/post wrapped functions.
|
||
|
_CallableT = TypeVar('_CallableT', bound=Callable)
|
||
|
|
||
|
|
||
|
@overload
|
||
|
def flagsaver(*args: Tuple[flags.FlagHolder, Any],
|
||
|
**kwargs: Any) -> '_FlagOverrider':
|
||
|
...
|
||
|
|
||
|
|
||
|
@overload
|
||
|
def flagsaver(func: _CallableT) -> _CallableT:
|
||
|
...
|
||
|
|
||
|
|
||
|
def flagsaver(*args, **kwargs):
|
||
|
"""The main flagsaver interface. See module doc for usage."""
|
||
|
return _construct_overrider(_FlagOverrider, *args, **kwargs)
|
||
|
|
||
|
|
||
|
@overload
|
||
|
def as_parsed(*args: Tuple[flags.FlagHolder, Union[str, Sequence[str]]],
|
||
|
**kwargs: Union[str, Sequence[str]]) -> '_ParsingFlagOverrider':
|
||
|
...
|
||
|
|
||
|
|
||
|
@overload
|
||
|
def as_parsed(func: _CallableT) -> _CallableT:
|
||
|
...
|
||
|
|
||
|
|
||
|
def as_parsed(*args, **kwargs):
|
||
|
"""Overrides flags by parsing strings, saves flag state similar to flagsaver.
|
||
|
|
||
|
This function can be used as either a decorator or context manager similar to
|
||
|
flagsaver.flagsaver(). However, where flagsaver.flagsaver() directly sets the
|
||
|
flags to new values, this function will parse the provided arguments as if
|
||
|
they were provided on the command line. Among other things, this will cause
|
||
|
`FLAGS['flag_name'].parsed == True`.
|
||
|
|
||
|
A note on unparsed input: For many flag types, the unparsed version will be
|
||
|
a single string. However for multi_x (multi_string, multi_integer, multi_enum)
|
||
|
the unparsed version will be a Sequence of strings.
|
||
|
|
||
|
Args:
|
||
|
*args: Tuples of FlagHolders and their unparsed value.
|
||
|
**kwargs: The keyword args are flag names, and the values are unparsed
|
||
|
values.
|
||
|
|
||
|
Returns:
|
||
|
_ParsingFlagOverrider that serves as a context manager or decorator. Will
|
||
|
save previous flag state and parse new flags, then on cleanup it will
|
||
|
restore the previous flag state.
|
||
|
"""
|
||
|
return _construct_overrider(_ParsingFlagOverrider, *args, **kwargs)
|
||
|
|
||
|
|
||
|
# NOTE: the order of these overload declarations matters. The type checker will
|
||
|
# pick the first match which could be incorrect.
|
||
|
@overload
|
||
|
def _construct_overrider(
|
||
|
flag_overrider_cls: Type['_ParsingFlagOverrider'],
|
||
|
*args: Tuple[flags.FlagHolder, Union[str, Sequence[str]]],
|
||
|
**kwargs: Union[str, Sequence[str]]) -> '_ParsingFlagOverrider':
|
||
|
...
|
||
|
|
||
|
|
||
|
@overload
|
||
|
def _construct_overrider(flag_overrider_cls: Type['_FlagOverrider'],
|
||
|
*args: Tuple[flags.FlagHolder, Any],
|
||
|
**kwargs: Any) -> '_FlagOverrider':
|
||
|
...
|
||
|
|
||
|
|
||
|
@overload
|
||
|
def _construct_overrider(flag_overrider_cls: Type['_FlagOverrider'],
|
||
|
func: _CallableT) -> _CallableT:
|
||
|
...
|
||
|
|
||
|
|
||
|
def _construct_overrider(flag_overrider_cls, *args, **kwargs):
|
||
|
"""Handles the args/kwargs returning an instance of flag_overrider_cls.
|
||
|
|
||
|
If flag_overrider_cls is _FlagOverrider then values should be native python
|
||
|
types matching the python types. Otherwise if flag_overrider_cls is
|
||
|
_ParsingFlagOverrider the values should be strings or sequences of strings.
|
||
|
|
||
|
Args:
|
||
|
flag_overrider_cls: The class that will do the overriding.
|
||
|
*args: Tuples of FlagHolder and the new flag value.
|
||
|
**kwargs: Keword args mapping flag name to new flag value.
|
||
|
|
||
|
Returns:
|
||
|
A _FlagOverrider to be used as a decorator or context manager.
|
||
|
"""
|
||
|
if not args:
|
||
|
return flag_overrider_cls(**kwargs)
|
||
|
# args can be [func] if used as `@flagsaver` instead of `@flagsaver(...)`
|
||
|
if len(args) == 1 and callable(args[0]):
|
||
|
if kwargs:
|
||
|
raise ValueError(
|
||
|
"It's invalid to specify both positional and keyword parameters.")
|
||
|
func = args[0]
|
||
|
if inspect.isclass(func):
|
||
|
raise TypeError('@flagsaver.flagsaver cannot be applied to a class.')
|
||
|
return _wrap(flag_overrider_cls, func, {})
|
||
|
# args can be a list of (FlagHolder, value) pairs.
|
||
|
# In which case they augment any specified kwargs.
|
||
|
for arg in args:
|
||
|
if not isinstance(arg, tuple) or len(arg) != 2:
|
||
|
raise ValueError('Expected (FlagHolder, value) pair, found %r' % (arg,))
|
||
|
holder, value = arg
|
||
|
if not isinstance(holder, flags.FlagHolder):
|
||
|
raise ValueError('Expected (FlagHolder, value) pair, found %r' % (arg,))
|
||
|
if holder.name in kwargs:
|
||
|
raise ValueError('Cannot set --%s multiple times' % holder.name)
|
||
|
kwargs[holder.name] = value
|
||
|
return flag_overrider_cls(**kwargs)
|
||
|
|
||
|
|
||
|
def save_flag_values(
|
||
|
flag_values: flags.FlagValues = FLAGS) -> Mapping[str, Mapping[str, Any]]:
|
||
|
"""Returns copy of flag values as a dict.
|
||
|
|
||
|
Args:
|
||
|
flag_values: FlagValues, the FlagValues instance with which the flag will be
|
||
|
saved. This should almost never need to be overridden.
|
||
|
|
||
|
Returns:
|
||
|
Dictionary mapping keys to values. Keys are flag names, values are
|
||
|
corresponding ``__dict__`` members. E.g. ``{'key': value_dict, ...}``.
|
||
|
"""
|
||
|
return {name: _copy_flag_dict(flag_values[name]) for name in flag_values}
|
||
|
|
||
|
|
||
|
def restore_flag_values(saved_flag_values: Mapping[str, Mapping[str, Any]],
|
||
|
flag_values: flags.FlagValues = FLAGS):
|
||
|
"""Restores flag values based on the dictionary of flag values.
|
||
|
|
||
|
Args:
|
||
|
saved_flag_values: {'flag_name': value_dict, ...}
|
||
|
flag_values: FlagValues, the FlagValues instance from which the flag will be
|
||
|
restored. This should almost never need to be overridden.
|
||
|
"""
|
||
|
new_flag_names = list(flag_values)
|
||
|
for name in new_flag_names:
|
||
|
saved = saved_flag_values.get(name)
|
||
|
if saved is None:
|
||
|
# If __dict__ was not saved delete "new" flag.
|
||
|
delattr(flag_values, name)
|
||
|
else:
|
||
|
if flag_values[name].value != saved['_value']:
|
||
|
flag_values[name].value = saved['_value'] # Ensure C++ value is set.
|
||
|
flag_values[name].__dict__ = saved
|
||
|
|
||
|
|
||
|
@overload
|
||
|
def _wrap(flag_overrider_cls: Type['_FlagOverrider'], func: _CallableT,
|
||
|
overrides: Mapping[str, Any]) -> _CallableT:
|
||
|
...
|
||
|
|
||
|
|
||
|
@overload
|
||
|
def _wrap(flag_overrider_cls: Type['_ParsingFlagOverrider'], func: _CallableT,
|
||
|
overrides: Mapping[str, Union[str, Sequence[str]]]) -> _CallableT:
|
||
|
...
|
||
|
|
||
|
|
||
|
def _wrap(flag_overrider_cls, func, overrides):
|
||
|
"""Creates a wrapper function that saves/restores flag values.
|
||
|
|
||
|
Args:
|
||
|
flag_overrider_cls: The class that will be used as a context manager.
|
||
|
func: This will be called between saving flags and restoring flags.
|
||
|
overrides: Flag names mapped to their values. These flags will be set after
|
||
|
saving the original flag state. The type of the values depends on if
|
||
|
_FlagOverrider or _ParsingFlagOverrider was specified.
|
||
|
|
||
|
Returns:
|
||
|
A wrapped version of func.
|
||
|
"""
|
||
|
|
||
|
@functools.wraps(func)
|
||
|
def _flagsaver_wrapper(*args, **kwargs):
|
||
|
"""Wrapper function that saves and restores flags."""
|
||
|
with flag_overrider_cls(**overrides):
|
||
|
return func(*args, **kwargs)
|
||
|
|
||
|
return _flagsaver_wrapper
|
||
|
|
||
|
|
||
|
class _FlagOverrider(object):
|
||
|
"""Overrides flags for the duration of the decorated function call.
|
||
|
|
||
|
It also restores all original values of flags after decorated method
|
||
|
completes.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, **overrides: Any):
|
||
|
self._overrides = overrides
|
||
|
self._saved_flag_values = None
|
||
|
|
||
|
def __call__(self, func: _CallableT) -> _CallableT:
|
||
|
if inspect.isclass(func):
|
||
|
raise TypeError('flagsaver cannot be applied to a class.')
|
||
|
return _wrap(self.__class__, func, self._overrides)
|
||
|
|
||
|
def __enter__(self):
|
||
|
self._saved_flag_values = save_flag_values(FLAGS)
|
||
|
try:
|
||
|
FLAGS._set_attributes(**self._overrides)
|
||
|
except:
|
||
|
# It may fail because of flag validators.
|
||
|
restore_flag_values(self._saved_flag_values, FLAGS)
|
||
|
raise
|
||
|
|
||
|
def __exit__(self, exc_type, exc_value, traceback):
|
||
|
restore_flag_values(self._saved_flag_values, FLAGS)
|
||
|
|
||
|
|
||
|
class _ParsingFlagOverrider(_FlagOverrider):
|
||
|
"""Context manager for overriding flags.
|
||
|
|
||
|
Simulates command line parsing.
|
||
|
|
||
|
This is simlar to _FlagOverrider except that all **overrides should be
|
||
|
strings or sequences of strings, and when context is entered this class calls
|
||
|
.parse(value)
|
||
|
|
||
|
This results in the flags having .present set properly.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, **overrides: Union[str, Sequence[str]]):
|
||
|
for flag_name, new_value in overrides.items():
|
||
|
if isinstance(new_value, str):
|
||
|
continue
|
||
|
if (isinstance(new_value, collections.abc.Sequence) and
|
||
|
all(isinstance(single_value, str) for single_value in new_value)):
|
||
|
continue
|
||
|
raise TypeError(
|
||
|
f'flagsaver.as_parsed() cannot parse {flag_name}. Expected a single '
|
||
|
f'string or sequence of strings but {type(new_value)} was provided.')
|
||
|
super().__init__(**overrides)
|
||
|
|
||
|
def __enter__(self):
|
||
|
self._saved_flag_values = save_flag_values(FLAGS)
|
||
|
try:
|
||
|
for flag_name, unparsed_value in self._overrides.items():
|
||
|
# LINT.IfChange(flag_override_parsing)
|
||
|
FLAGS[flag_name].parse(unparsed_value)
|
||
|
FLAGS[flag_name].using_default_value = False
|
||
|
# LINT.ThenChange()
|
||
|
|
||
|
# Perform the validation on all modified flags. This is something that
|
||
|
# FLAGS._set_attributes() does for you in _FlagOverrider.
|
||
|
for flag_name in self._overrides:
|
||
|
FLAGS._assert_validators(FLAGS[flag_name].validators)
|
||
|
|
||
|
except KeyError as e:
|
||
|
# If a flag doesn't exist, an UnrecognizedFlagError is more specific.
|
||
|
restore_flag_values(self._saved_flag_values, FLAGS)
|
||
|
raise flags.UnrecognizedFlagError('Unknown command line flag.') from e
|
||
|
|
||
|
except:
|
||
|
# It may fail because of flag validators or general parsing issues.
|
||
|
restore_flag_values(self._saved_flag_values, FLAGS)
|
||
|
raise
|
||
|
|
||
|
|
||
|
def _copy_flag_dict(flag: flags.Flag) -> Mapping[str, Any]:
|
||
|
"""Returns a copy of the flag object's ``__dict__``.
|
||
|
|
||
|
It's mostly a shallow copy of the ``__dict__``, except it also does a shallow
|
||
|
copy of the validator list.
|
||
|
|
||
|
Args:
|
||
|
flag: flags.Flag, the flag to copy.
|
||
|
|
||
|
Returns:
|
||
|
A copy of the flag object's ``__dict__``.
|
||
|
"""
|
||
|
copy = flag.__dict__.copy()
|
||
|
copy['_value'] = flag.value # Ensure correct restore for C++ flags.
|
||
|
copy['validators'] = list(flag.validators)
|
||
|
return copy
|