Spaces:
Running
Running
File size: 13,392 Bytes
375a1cf |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 |
# 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'].present == 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
|