scripts/validation_utils.py (46 lines of code) (raw):

#!/usr/bin/env python3 # Copyright 2017 Google Inc. All Rights Reserved. # # 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. """Utilities for schema and command line validation""" import argparse import re # For easier development, we allow redefining builtins like # --substitutions=PROJECT_ID=foo even though gcloud doesn't. KEY_VALUE_REGEX = re.compile(r'^([A-Z_][A-Z0-9_]*)=(.*)$') def get_field_value(container, field_name, field_type): """Fetch a field from a container with typechecking and default values. The field value is coerced to the desired type. If the field is not present, an instance of `field_type` is constructed with no arguments and used as the default value. This function exists because yaml parsing can lead to surprising outputs, and the resulting errors are confusing. For example: entrypoint1: a string, but I can accidentally treat as an sequence entrypoint2: [a, list, but, I, might, think, its, a, string] version1: 3 # Parsed to int version2: 3.1 # Parsed to float version3: 3.1.1 # Parsed to str feature: off # Parsed to the boolean False Args: container (dict): Object decoded from yaml field_name (str): Field that should be present in `container` field_type (type): Expected type for field value Returns: Any: Fetched or default value of field Raises: ValueError: if field value cannot be converted to the desired type """ try: value = container[field_name] if value is None: return field_type() except (IndexError, KeyError): return field_type() msg = 'Expected "{}" field to be of type "{}", but found type "{}"' if not isinstance(value, field_type): # list('some string') is a successful type cast as far as Python # is concerned, but doesn't exactly produce the results we want. # We have a whitelist of conversions we will attempt. whitelist = ( (float, str), (int, str), (str, float), (str, int), (int, float), ) if (type(value), field_type) not in whitelist: raise ValueError(msg.format(field_name, field_type, type(value))) try: value = field_type(value) except ValueError as e: e.message = msg.format(field_name, field_type, type(value)) raise return value def validate_arg_regex(flag_value, flag_regex): """Check a named command line flag against a regular expression""" if not re.match(flag_regex, flag_value): raise argparse.ArgumentTypeError( 'Value "{}" does not match pattern "{}"'.format( flag_value, flag_regex.pattern)) return flag_value def validate_arg_dict(flag_value): """Parse a command line flag as a key=val,... dict""" if not flag_value: return {} entries = flag_value.split(',') pairs = [] for entry in entries: match = re.match(KEY_VALUE_REGEX, entry) if not match: raise argparse.ArgumentTypeError( 'Value "{}" should be a list like _KEY1=value1,_KEY2=value2"'. format(flag_value)) pairs.append((match.group(1), match.group(2))) return dict(pairs)