1#!/usr/bin/env python 2# 3# Copyright 2014 Google Inc. All rights reserved. 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16# 17 18"""Common utility library.""" 19 20import functools 21import inspect 22import logging 23 24import six 25from six.moves import urllib 26 27 28__author__ = [ 29 'rafek@google.com (Rafe Kaplan)', 30 'guido@google.com (Guido van Rossum)', 31] 32 33__all__ = [ 34 'positional', 35 'POSITIONAL_WARNING', 36 'POSITIONAL_EXCEPTION', 37 'POSITIONAL_IGNORE', 38] 39 40logger = logging.getLogger(__name__) 41 42POSITIONAL_WARNING = 'WARNING' 43POSITIONAL_EXCEPTION = 'EXCEPTION' 44POSITIONAL_IGNORE = 'IGNORE' 45POSITIONAL_SET = frozenset([POSITIONAL_WARNING, POSITIONAL_EXCEPTION, 46 POSITIONAL_IGNORE]) 47 48positional_parameters_enforcement = POSITIONAL_WARNING 49 50 51def positional(max_positional_args): 52 """A decorator to declare that only the first N arguments my be positional. 53 54 This decorator makes it easy to support Python 3 style keyword-only 55 parameters. For example, in Python 3 it is possible to write:: 56 57 def fn(pos1, *, kwonly1=None, kwonly1=None): 58 ... 59 60 All named parameters after ``*`` must be a keyword:: 61 62 fn(10, 'kw1', 'kw2') # Raises exception. 63 fn(10, kwonly1='kw1') # Ok. 64 65 Example 66 ^^^^^^^ 67 68 To define a function like above, do:: 69 70 @positional(1) 71 def fn(pos1, kwonly1=None, kwonly2=None): 72 ... 73 74 If no default value is provided to a keyword argument, it becomes a 75 required keyword argument:: 76 77 @positional(0) 78 def fn(required_kw): 79 ... 80 81 This must be called with the keyword parameter:: 82 83 fn() # Raises exception. 84 fn(10) # Raises exception. 85 fn(required_kw=10) # Ok. 86 87 When defining instance or class methods always remember to account for 88 ``self`` and ``cls``:: 89 90 class MyClass(object): 91 92 @positional(2) 93 def my_method(self, pos1, kwonly1=None): 94 ... 95 96 @classmethod 97 @positional(2) 98 def my_method(cls, pos1, kwonly1=None): 99 ... 100 101 The positional decorator behavior is controlled by 102 ``util.positional_parameters_enforcement``, which may be set to 103 ``POSITIONAL_EXCEPTION``, ``POSITIONAL_WARNING`` or 104 ``POSITIONAL_IGNORE`` to raise an exception, log a warning, or do 105 nothing, respectively, if a declaration is violated. 106 107 Args: 108 max_positional_arguments: Maximum number of positional arguments. All 109 parameters after the this index must be 110 keyword only. 111 112 Returns: 113 A decorator that prevents using arguments after max_positional_args 114 from being used as positional parameters. 115 116 Raises: 117 TypeError: if a key-word only argument is provided as a positional 118 parameter, but only if 119 util.positional_parameters_enforcement is set to 120 POSITIONAL_EXCEPTION. 121 """ 122 123 def positional_decorator(wrapped): 124 @functools.wraps(wrapped) 125 def positional_wrapper(*args, **kwargs): 126 if len(args) > max_positional_args: 127 plural_s = '' 128 if max_positional_args != 1: 129 plural_s = 's' 130 message = ('%s() takes at most %d positional ' 131 'argument%s (%d given)' % ( 132 wrapped.__name__, max_positional_args, 133 plural_s, len(args))) 134 if positional_parameters_enforcement == POSITIONAL_EXCEPTION: 135 raise TypeError(message) 136 elif positional_parameters_enforcement == POSITIONAL_WARNING: 137 logger.warning(message) 138 else: # IGNORE 139 pass 140 return wrapped(*args, **kwargs) 141 return positional_wrapper 142 143 if isinstance(max_positional_args, six.integer_types): 144 return positional_decorator 145 else: 146 args, _, _, defaults = inspect.getargspec(max_positional_args) 147 return positional(len(args) - len(defaults))(max_positional_args) 148 149 150def scopes_to_string(scopes): 151 """Converts scope value to a string. 152 153 If scopes is a string then it is simply passed through. If scopes is an 154 iterable then a string is returned that is all the individual scopes 155 concatenated with spaces. 156 157 Args: 158 scopes: string or iterable of strings, the scopes. 159 160 Returns: 161 The scopes formatted as a single string. 162 """ 163 if isinstance(scopes, six.string_types): 164 return scopes 165 else: 166 return ' '.join(scopes) 167 168 169def string_to_scopes(scopes): 170 """Converts stringifed scope value to a list. 171 172 If scopes is a list then it is simply passed through. If scopes is an 173 string then a list of each individual scope is returned. 174 175 Args: 176 scopes: a string or iterable of strings, the scopes. 177 178 Returns: 179 The scopes in a list. 180 """ 181 if not scopes: 182 return [] 183 if isinstance(scopes, six.string_types): 184 return scopes.split(' ') 185 else: 186 return scopes 187 188 189def dict_to_tuple_key(dictionary): 190 """Converts a dictionary to a tuple that can be used as an immutable key. 191 192 The resulting key is always sorted so that logically equivalent 193 dictionaries always produce an identical tuple for a key. 194 195 Args: 196 dictionary: the dictionary to use as the key. 197 198 Returns: 199 A tuple representing the dictionary in it's naturally sorted ordering. 200 """ 201 return tuple(sorted(dictionary.items())) 202 203 204def _add_query_parameter(url, name, value): 205 """Adds a query parameter to a url. 206 207 Replaces the current value if it already exists in the URL. 208 209 Args: 210 url: string, url to add the query parameter to. 211 name: string, query parameter name. 212 value: string, query parameter value. 213 214 Returns: 215 Updated query parameter. Does not update the url if value is None. 216 """ 217 if value is None: 218 return url 219 else: 220 parsed = list(urllib.parse.urlparse(url)) 221 q = dict(urllib.parse.parse_qsl(parsed[4])) 222 q[name] = value 223 parsed[4] = urllib.parse.urlencode(q) 224 return urllib.parse.urlunparse(parsed) 225