1# Copyright 2015 Google Inc. All rights reserved. 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"""Helper functions for commonly used utilities.""" 16 17import functools 18import inspect 19import logging 20import warnings 21 22import six 23from six.moves import urllib 24 25 26logger = logging.getLogger(__name__) 27 28POSITIONAL_WARNING = 'WARNING' 29POSITIONAL_EXCEPTION = 'EXCEPTION' 30POSITIONAL_IGNORE = 'IGNORE' 31POSITIONAL_SET = frozenset([POSITIONAL_WARNING, POSITIONAL_EXCEPTION, 32 POSITIONAL_IGNORE]) 33 34positional_parameters_enforcement = POSITIONAL_WARNING 35 36_SYM_LINK_MESSAGE = 'File: {0}: Is a symbolic link.' 37_IS_DIR_MESSAGE = '{0}: Is a directory' 38_MISSING_FILE_MESSAGE = 'Cannot access {0}: No such file or directory' 39 40 41def positional(max_positional_args): 42 """A decorator to declare that only the first N arguments my be positional. 43 44 This decorator makes it easy to support Python 3 style keyword-only 45 parameters. For example, in Python 3 it is possible to write:: 46 47 def fn(pos1, *, kwonly1=None, kwonly1=None): 48 ... 49 50 All named parameters after ``*`` must be a keyword:: 51 52 fn(10, 'kw1', 'kw2') # Raises exception. 53 fn(10, kwonly1='kw1') # Ok. 54 55 Example 56 ^^^^^^^ 57 58 To define a function like above, do:: 59 60 @positional(1) 61 def fn(pos1, kwonly1=None, kwonly2=None): 62 ... 63 64 If no default value is provided to a keyword argument, it becomes a 65 required keyword argument:: 66 67 @positional(0) 68 def fn(required_kw): 69 ... 70 71 This must be called with the keyword parameter:: 72 73 fn() # Raises exception. 74 fn(10) # Raises exception. 75 fn(required_kw=10) # Ok. 76 77 When defining instance or class methods always remember to account for 78 ``self`` and ``cls``:: 79 80 class MyClass(object): 81 82 @positional(2) 83 def my_method(self, pos1, kwonly1=None): 84 ... 85 86 @classmethod 87 @positional(2) 88 def my_method(cls, pos1, kwonly1=None): 89 ... 90 91 The positional decorator behavior is controlled by 92 ``_helpers.positional_parameters_enforcement``, which may be set to 93 ``POSITIONAL_EXCEPTION``, ``POSITIONAL_WARNING`` or 94 ``POSITIONAL_IGNORE`` to raise an exception, log a warning, or do 95 nothing, respectively, if a declaration is violated. 96 97 Args: 98 max_positional_arguments: Maximum number of positional arguments. All 99 parameters after the this index must be 100 keyword only. 101 102 Returns: 103 A decorator that prevents using arguments after max_positional_args 104 from being used as positional parameters. 105 106 Raises: 107 TypeError: if a key-word only argument is provided as a positional 108 parameter, but only if 109 _helpers.positional_parameters_enforcement is set to 110 POSITIONAL_EXCEPTION. 111 """ 112 113 def positional_decorator(wrapped): 114 @functools.wraps(wrapped) 115 def positional_wrapper(*args, **kwargs): 116 if len(args) > max_positional_args: 117 plural_s = '' 118 if max_positional_args != 1: 119 plural_s = 's' 120 message = ('{function}() takes at most {args_max} positional ' 121 'argument{plural} ({args_given} given)'.format( 122 function=wrapped.__name__, 123 args_max=max_positional_args, 124 args_given=len(args), 125 plural=plural_s)) 126 if positional_parameters_enforcement == POSITIONAL_EXCEPTION: 127 raise TypeError(message) 128 elif positional_parameters_enforcement == POSITIONAL_WARNING: 129 logger.warning(message) 130 return wrapped(*args, **kwargs) 131 return positional_wrapper 132 133 if isinstance(max_positional_args, six.integer_types): 134 return positional_decorator 135 else: 136 args, _, _, defaults = inspect.getargspec(max_positional_args) 137 return positional(len(args) - len(defaults))(max_positional_args) 138 139 140def parse_unique_urlencoded(content): 141 """Parses unique key-value parameters from urlencoded content. 142 143 Args: 144 content: string, URL-encoded key-value pairs. 145 146 Returns: 147 dict, The key-value pairs from ``content``. 148 149 Raises: 150 ValueError: if one of the keys is repeated. 151 """ 152 urlencoded_params = urllib.parse.parse_qs(content) 153 params = {} 154 for key, value in six.iteritems(urlencoded_params): 155 if len(value) != 1: 156 msg = ('URL-encoded content contains a repeated value:' 157 '%s -> %s' % (key, ', '.join(value))) 158 raise ValueError(msg) 159 params[key] = value[0] 160 return params 161 162 163def update_query_params(uri, params): 164 """Updates a URI with new query parameters. 165 166 If a given key from ``params`` is repeated in the ``uri``, then 167 the URI will be considered invalid and an error will occur. 168 169 If the URI is valid, then each value from ``params`` will 170 replace the corresponding value in the query parameters (if 171 it exists). 172 173 Args: 174 uri: string, A valid URI, with potential existing query parameters. 175 params: dict, A dictionary of query parameters. 176 177 Returns: 178 The same URI but with the new query parameters added. 179 """ 180 parts = urllib.parse.urlparse(uri) 181 query_params = parse_unique_urlencoded(parts.query) 182 query_params.update(params) 183 new_query = urllib.parse.urlencode(query_params) 184 new_parts = parts._replace(query=new_query) 185 return urllib.parse.urlunparse(new_parts) 186 187 188def _add_query_parameter(url, name, value): 189 """Adds a query parameter to a url. 190 191 Replaces the current value if it already exists in the URL. 192 193 Args: 194 url: string, url to add the query parameter to. 195 name: string, query parameter name. 196 value: string, query parameter value. 197 198 Returns: 199 Updated query parameter. Does not update the url if value is None. 200 """ 201 if value is None: 202 return url 203 else: 204 return update_query_params(url, {name: value}) 205