1# Copyright 2017 The Abseil Authors.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Module to enforce different constraints on flags.
16
17Flags validators can be registered using following functions / decorators::
18
19 flags.register_validator
20 @flags.validator
21 flags.register_multi_flags_validator
22 @flags.multi_flags_validator
23
24Three convenience functions are also provided for common flag constraints::
25
26 flags.mark_flag_as_required
27 flags.mark_flags_as_required
28 flags.mark_flags_as_mutual_exclusive
29 flags.mark_bool_flags_as_mutual_exclusive
30
31See their docstring in this module for a usage manual.
32
33Do NOT import this module directly. Import the flags package and use the
34aliases defined at the package level instead.
35"""
36
37import warnings
38
39from absl.flags import _exceptions
40from absl.flags import _flagvalues
41from absl.flags import _validators_classes
42
43
44def register_validator(flag_name,
45 checker,
46 message='Flag validation failed',
47 flag_values=_flagvalues.FLAGS):
48 """Adds a constraint, which will be enforced during program execution.
49
50 The constraint is validated when flags are initially parsed, and after each
51 change of the corresponding flag's value.
52
53 Args:
54 flag_name: str | FlagHolder, name or holder of the flag to be checked.
55 Positional-only parameter.
56 checker: callable, a function to validate the flag.
57
58 * input - A single positional argument: The value of the corresponding
59 flag (string, boolean, etc. This value will be passed to checker
60 by the library).
61 * output - bool, True if validator constraint is satisfied.
62 If constraint is not satisfied, it should either ``return False`` or
63 ``raise flags.ValidationError(desired_error_message)``.
64
65 message: str, error text to be shown to the user if checker returns False.
66 If checker raises flags.ValidationError, message from the raised
67 error will be shown.
68 flag_values: flags.FlagValues, optional FlagValues instance to validate
69 against.
70
71 Raises:
72 AttributeError: Raised when flag_name is not registered as a valid flag
73 name.
74 ValueError: Raised when flag_values is non-default and does not match the
75 FlagValues of the provided FlagHolder instance.
76 """
77 flag_name, flag_values = _flagvalues.resolve_flag_ref(flag_name, flag_values)
78 v = _validators_classes.SingleFlagValidator(flag_name, checker, message)
79 _add_validator(flag_values, v)
80
81
82def validator(flag_name, message='Flag validation failed',
83 flag_values=_flagvalues.FLAGS):
84 """A function decorator for defining a flag validator.
85
86 Registers the decorated function as a validator for flag_name, e.g.::
87
88 @flags.validator('foo')
89 def _CheckFoo(foo):
90 ...
91
92 See :func:`register_validator` for the specification of checker function.
93
94 Args:
95 flag_name: str | FlagHolder, name or holder of the flag to be checked.
96 Positional-only parameter.
97 message: str, error text to be shown to the user if checker returns False.
98 If checker raises flags.ValidationError, message from the raised
99 error will be shown.
100 flag_values: flags.FlagValues, optional FlagValues instance to validate
101 against.
102 Returns:
103 A function decorator that registers its function argument as a validator.
104 Raises:
105 AttributeError: Raised when flag_name is not registered as a valid flag
106 name.
107 """
108
109 def decorate(function):
110 register_validator(flag_name, function,
111 message=message,
112 flag_values=flag_values)
113 return function
114 return decorate
115
116
117def register_multi_flags_validator(flag_names,
118 multi_flags_checker,
119 message='Flags validation failed',
120 flag_values=_flagvalues.FLAGS):
121 """Adds a constraint to multiple flags.
122
123 The constraint is validated when flags are initially parsed, and after each
124 change of the corresponding flag's value.
125
126 Args:
127 flag_names: [str | FlagHolder], a list of the flag names or holders to be
128 checked. Positional-only parameter.
129 multi_flags_checker: callable, a function to validate the flag.
130
131 * input - dict, with keys() being flag_names, and value for each key
132 being the value of the corresponding flag (string, boolean, etc).
133 * output - bool, True if validator constraint is satisfied.
134 If constraint is not satisfied, it should either return False or
135 raise flags.ValidationError.
136
137 message: str, error text to be shown to the user if checker returns False.
138 If checker raises flags.ValidationError, message from the raised
139 error will be shown.
140 flag_values: flags.FlagValues, optional FlagValues instance to validate
141 against.
142
143 Raises:
144 AttributeError: Raised when a flag is not registered as a valid flag name.
145 ValueError: Raised when multiple FlagValues are used in the same
146 invocation. This can occur when FlagHolders have different `_flagvalues`
147 or when str-type flag_names entries are present and the `flag_values`
148 argument does not match that of provided FlagHolder(s).
149 """
150 flag_names, flag_values = _flagvalues.resolve_flag_refs(
151 flag_names, flag_values)
152 v = _validators_classes.MultiFlagsValidator(
153 flag_names, multi_flags_checker, message)
154 _add_validator(flag_values, v)
155
156
157def multi_flags_validator(flag_names,
158 message='Flag validation failed',
159 flag_values=_flagvalues.FLAGS):
160 """A function decorator for defining a multi-flag validator.
161
162 Registers the decorated function as a validator for flag_names, e.g.::
163
164 @flags.multi_flags_validator(['foo', 'bar'])
165 def _CheckFooBar(flags_dict):
166 ...
167
168 See :func:`register_multi_flags_validator` for the specification of checker
169 function.
170
171 Args:
172 flag_names: [str | FlagHolder], a list of the flag names or holders to be
173 checked. Positional-only parameter.
174 message: str, error text to be shown to the user if checker returns False.
175 If checker raises flags.ValidationError, message from the raised
176 error will be shown.
177 flag_values: flags.FlagValues, optional FlagValues instance to validate
178 against.
179
180 Returns:
181 A function decorator that registers its function argument as a validator.
182
183 Raises:
184 AttributeError: Raised when a flag is not registered as a valid flag name.
185 """
186
187 def decorate(function):
188 register_multi_flags_validator(flag_names,
189 function,
190 message=message,
191 flag_values=flag_values)
192 return function
193
194 return decorate
195
196
197def mark_flag_as_required(flag_name, flag_values=_flagvalues.FLAGS):
198 """Ensures that flag is not None during program execution.
199
200 Registers a flag validator, which will follow usual validator rules.
201 Important note: validator will pass for any non-``None`` value, such as
202 ``False``, ``0`` (zero), ``''`` (empty string) and so on.
203
204 If your module might be imported by others, and you only wish to make the flag
205 required when the module is directly executed, call this method like this::
206
207 if __name__ == '__main__':
208 flags.mark_flag_as_required('your_flag_name')
209 app.run()
210
211 Args:
212 flag_name: str | FlagHolder, name or holder of the flag.
213 Positional-only parameter.
214 flag_values: flags.FlagValues, optional :class:`~absl.flags.FlagValues`
215 instance where the flag is defined.
216 Raises:
217 AttributeError: Raised when flag_name is not registered as a valid flag
218 name.
219 ValueError: Raised when flag_values is non-default and does not match the
220 FlagValues of the provided FlagHolder instance.
221 """
222 flag_name, flag_values = _flagvalues.resolve_flag_ref(flag_name, flag_values)
223 if flag_values[flag_name].default is not None:
224 warnings.warn(
225 'Flag --%s has a non-None default value; therefore, '
226 'mark_flag_as_required will pass even if flag is not specified in the '
227 'command line!' % flag_name,
228 stacklevel=2)
229 register_validator(
230 flag_name,
231 lambda value: value is not None,
232 message=f'Flag --{flag_name} must have a value other than None.',
233 flag_values=flag_values,
234 )
235
236
237def mark_flags_as_required(flag_names, flag_values=_flagvalues.FLAGS):
238 """Ensures that flags are not None during program execution.
239
240 If your module might be imported by others, and you only wish to make the flag
241 required when the module is directly executed, call this method like this::
242
243 if __name__ == '__main__':
244 flags.mark_flags_as_required(['flag1', 'flag2', 'flag3'])
245 app.run()
246
247 Args:
248 flag_names: Sequence[str | FlagHolder], names or holders of the flags.
249 flag_values: flags.FlagValues, optional FlagValues instance where the flags
250 are defined.
251 Raises:
252 AttributeError: If any of flag name has not already been defined as a flag.
253 """
254 for flag_name in flag_names:
255 mark_flag_as_required(flag_name, flag_values)
256
257
258def mark_flags_as_mutual_exclusive(flag_names, required=False,
259 flag_values=_flagvalues.FLAGS):
260 """Ensures that only one flag among flag_names is not None.
261
262 Important note: This validator checks if flag values are ``None``, and it does
263 not distinguish between default and explicit values. Therefore, this validator
264 does not make sense when applied to flags with default values other than None,
265 including other false values (e.g. ``False``, ``0``, ``''``, ``[]``). That
266 includes multi flags with a default value of ``[]`` instead of None.
267
268 Args:
269 flag_names: [str | FlagHolder], names or holders of flags.
270 Positional-only parameter.
271 required: bool. If true, exactly one of the flags must have a value other
272 than None. Otherwise, at most one of the flags can have a value other
273 than None, and it is valid for all of the flags to be None.
274 flag_values: flags.FlagValues, optional FlagValues instance where the flags
275 are defined.
276
277 Raises:
278 ValueError: Raised when multiple FlagValues are used in the same
279 invocation. This can occur when FlagHolders have different `_flagvalues`
280 or when str-type flag_names entries are present and the `flag_values`
281 argument does not match that of provided FlagHolder(s).
282 """
283 flag_names, flag_values = _flagvalues.resolve_flag_refs(
284 flag_names, flag_values)
285 for flag_name in flag_names:
286 if flag_values[flag_name].default is not None:
287 warnings.warn(
288 'Flag --{} has a non-None default value. That does not make sense '
289 'with mark_flags_as_mutual_exclusive, which checks whether the '
290 'listed flags have a value other than None.'.format(flag_name),
291 stacklevel=2)
292
293 def validate_mutual_exclusion(flags_dict):
294 flag_count = sum(1 for val in flags_dict.values() if val is not None)
295 if flag_count == 1 or (not required and flag_count == 0):
296 return True
297 raise _exceptions.ValidationError(
298 '{} one of ({}) must have a value other than None.'.format(
299 'Exactly' if required else 'At most', ', '.join(flag_names)))
300
301 register_multi_flags_validator(
302 flag_names, validate_mutual_exclusion, flag_values=flag_values)
303
304
305def mark_bool_flags_as_mutual_exclusive(flag_names, required=False,
306 flag_values=_flagvalues.FLAGS):
307 """Ensures that only one flag among flag_names is True.
308
309 Args:
310 flag_names: [str | FlagHolder], names or holders of flags.
311 Positional-only parameter.
312 required: bool. If true, exactly one flag must be True. Otherwise, at most
313 one flag can be True, and it is valid for all flags to be False.
314 flag_values: flags.FlagValues, optional FlagValues instance where the flags
315 are defined.
316
317 Raises:
318 ValueError: Raised when multiple FlagValues are used in the same
319 invocation. This can occur when FlagHolders have different `_flagvalues`
320 or when str-type flag_names entries are present and the `flag_values`
321 argument does not match that of provided FlagHolder(s).
322 """
323 flag_names, flag_values = _flagvalues.resolve_flag_refs(
324 flag_names, flag_values)
325 for flag_name in flag_names:
326 if not flag_values[flag_name].boolean:
327 raise _exceptions.ValidationError(
328 'Flag --{} is not Boolean, which is required for flags used in '
329 'mark_bool_flags_as_mutual_exclusive.'.format(flag_name))
330
331 def validate_boolean_mutual_exclusion(flags_dict):
332 flag_count = sum(bool(val) for val in flags_dict.values())
333 if flag_count == 1 or (not required and flag_count == 0):
334 return True
335 raise _exceptions.ValidationError(
336 '{} one of ({}) must be True.'.format(
337 'Exactly' if required else 'At most', ', '.join(flag_names)))
338
339 register_multi_flags_validator(
340 flag_names, validate_boolean_mutual_exclusion, flag_values=flag_values)
341
342
343def _add_validator(fv, validator_instance):
344 """Register new flags validator to be checked.
345
346 Args:
347 fv: flags.FlagValues, the FlagValues instance to add the validator.
348 validator_instance: validators.Validator, the validator to add.
349 Raises:
350 KeyError: Raised when validators work with a non-existing flag.
351 """
352 for flag_name in validator_instance.get_flags_names():
353 fv[flag_name].validators.append(validator_instance)