1# Copyright 2014 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"""Common utility library.""" 16 17import functools 18import inspect 19import logging 20 21import six 22from six.moves import urllib 23 24 25__author__ = [ 26 'rafek@google.com (Rafe Kaplan)', 27 'guido@google.com (Guido van Rossum)', 28] 29 30__all__ = [ 31 'positional', 32 'POSITIONAL_WARNING', 33 'POSITIONAL_EXCEPTION', 34 'POSITIONAL_IGNORE', 35] 36 37logger = logging.getLogger(__name__) 38 39POSITIONAL_WARNING = 'WARNING' 40POSITIONAL_EXCEPTION = 'EXCEPTION' 41POSITIONAL_IGNORE = 'IGNORE' 42POSITIONAL_SET = frozenset([POSITIONAL_WARNING, POSITIONAL_EXCEPTION, 43 POSITIONAL_IGNORE]) 44 45positional_parameters_enforcement = POSITIONAL_WARNING 46 47 48def positional(max_positional_args): 49 """A decorator to declare that only the first N arguments my be positional. 50 51 This decorator makes it easy to support Python 3 style keyword-only 52 parameters. For example, in Python 3 it is possible to write:: 53 54 def fn(pos1, *, kwonly1=None, kwonly1=None): 55 ... 56 57 All named parameters after ``*`` must be a keyword:: 58 59 fn(10, 'kw1', 'kw2') # Raises exception. 60 fn(10, kwonly1='kw1') # Ok. 61 62 Example 63 ^^^^^^^ 64 65 To define a function like above, do:: 66 67 @positional(1) 68 def fn(pos1, kwonly1=None, kwonly2=None): 69 ... 70 71 If no default value is provided to a keyword argument, it becomes a 72 required keyword argument:: 73 74 @positional(0) 75 def fn(required_kw): 76 ... 77 78 This must be called with the keyword parameter:: 79 80 fn() # Raises exception. 81 fn(10) # Raises exception. 82 fn(required_kw=10) # Ok. 83 84 When defining instance or class methods always remember to account for 85 ``self`` and ``cls``:: 86 87 class MyClass(object): 88 89 @positional(2) 90 def my_method(self, pos1, kwonly1=None): 91 ... 92 93 @classmethod 94 @positional(2) 95 def my_method(cls, pos1, kwonly1=None): 96 ... 97 98 The positional decorator behavior is controlled by 99 ``util.positional_parameters_enforcement``, which may be set to 100 ``POSITIONAL_EXCEPTION``, ``POSITIONAL_WARNING`` or 101 ``POSITIONAL_IGNORE`` to raise an exception, log a warning, or do 102 nothing, respectively, if a declaration is violated. 103 104 Args: 105 max_positional_arguments: Maximum number of positional arguments. All 106 parameters after the this index must be 107 keyword only. 108 109 Returns: 110 A decorator that prevents using arguments after max_positional_args 111 from being used as positional parameters. 112 113 Raises: 114 TypeError: if a key-word only argument is provided as a positional 115 parameter, but only if 116 util.positional_parameters_enforcement is set to 117 POSITIONAL_EXCEPTION. 118 """ 119 120 def positional_decorator(wrapped): 121 @functools.wraps(wrapped) 122 def positional_wrapper(*args, **kwargs): 123 if len(args) > max_positional_args: 124 plural_s = '' 125 if max_positional_args != 1: 126 plural_s = 's' 127 message = ('{function}() takes at most {args_max} positional ' 128 'argument{plural} ({args_given} given)'.format( 129 function=wrapped.__name__, 130 args_max=max_positional_args, 131 args_given=len(args), 132 plural=plural_s)) 133 if positional_parameters_enforcement == POSITIONAL_EXCEPTION: 134 raise TypeError(message) 135 elif positional_parameters_enforcement == POSITIONAL_WARNING: 136 logger.warning(message) 137 return wrapped(*args, **kwargs) 138 return positional_wrapper 139 140 if isinstance(max_positional_args, six.integer_types): 141 return positional_decorator 142 else: 143 args, _, _, defaults = inspect.getargspec(max_positional_args) 144 return positional(len(args) - len(defaults))(max_positional_args) 145 146 147def scopes_to_string(scopes): 148 """Converts scope value to a string. 149 150 If scopes is a string then it is simply passed through. If scopes is an 151 iterable then a string is returned that is all the individual scopes 152 concatenated with spaces. 153 154 Args: 155 scopes: string or iterable of strings, the scopes. 156 157 Returns: 158 The scopes formatted as a single string. 159 """ 160 if isinstance(scopes, six.string_types): 161 return scopes 162 else: 163 return ' '.join(scopes) 164 165 166def string_to_scopes(scopes): 167 """Converts stringifed scope value to a list. 168 169 If scopes is a list then it is simply passed through. If scopes is an 170 string then a list of each individual scope is returned. 171 172 Args: 173 scopes: a string or iterable of strings, the scopes. 174 175 Returns: 176 The scopes in a list. 177 """ 178 if not scopes: 179 return [] 180 if isinstance(scopes, six.string_types): 181 return scopes.split(' ') 182 else: 183 return scopes 184 185 186def _add_query_parameter(url, name, value): 187 """Adds a query parameter to a url. 188 189 Replaces the current value if it already exists in the URL. 190 191 Args: 192 url: string, url to add the query parameter to. 193 name: string, query parameter name. 194 value: string, query parameter value. 195 196 Returns: 197 Updated query parameter. Does not update the url if value is None. 198 """ 199 if value is None: 200 return url 201 else: 202 parsed = list(urllib.parse.urlparse(url)) 203 q = dict(urllib.parse.parse_qsl(parsed[4])) 204 q[name] = value 205 parsed[4] = urllib.parse.urlencode(q) 206 return urllib.parse.urlunparse(parsed) 207