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 20__author__ = [ 21 'rafek@google.com (Rafe Kaplan)', 22 'guido@google.com (Guido van Rossum)', 23] 24 25__all__ = [ 26 'positional', 27 'POSITIONAL_WARNING', 28 'POSITIONAL_EXCEPTION', 29 'POSITIONAL_IGNORE', 30] 31 32import functools 33import inspect 34import logging 35import sys 36import types 37 38import six 39from six.moves import urllib 40 41 42logger = logging.getLogger(__name__) 43 44POSITIONAL_WARNING = 'WARNING' 45POSITIONAL_EXCEPTION = 'EXCEPTION' 46POSITIONAL_IGNORE = 'IGNORE' 47POSITIONAL_SET = frozenset([POSITIONAL_WARNING, POSITIONAL_EXCEPTION, 48 POSITIONAL_IGNORE]) 49 50positional_parameters_enforcement = POSITIONAL_WARNING 51 52def positional(max_positional_args): 53 """A decorator to declare that only the first N arguments my be positional. 54 55 This decorator makes it easy to support Python 3 style keyword-only 56 parameters. For example, in Python 3 it is possible to write:: 57 58 def fn(pos1, *, kwonly1=None, kwonly1=None): 59 ... 60 61 All named parameters after ``*`` must be a keyword:: 62 63 fn(10, 'kw1', 'kw2') # Raises exception. 64 fn(10, kwonly1='kw1') # Ok. 65 66 Example 67 ^^^^^^^ 68 69 To define a function like above, do:: 70 71 @positional(1) 72 def fn(pos1, kwonly1=None, kwonly2=None): 73 ... 74 75 If no default value is provided to a keyword argument, it becomes a required 76 keyword argument:: 77 78 @positional(0) 79 def fn(required_kw): 80 ... 81 82 This must be called with the keyword parameter:: 83 84 fn() # Raises exception. 85 fn(10) # Raises exception. 86 fn(required_kw=10) # Ok. 87 88 When defining instance or class methods always remember to account for 89 ``self`` and ``cls``:: 90 91 class MyClass(object): 92 93 @positional(2) 94 def my_method(self, pos1, kwonly1=None): 95 ... 96 97 @classmethod 98 @positional(2) 99 def my_method(cls, pos1, kwonly1=None): 100 ... 101 102 The positional decorator behavior is controlled by 103 ``util.positional_parameters_enforcement``, which may be set to 104 ``POSITIONAL_EXCEPTION``, ``POSITIONAL_WARNING`` or 105 ``POSITIONAL_IGNORE`` to raise an exception, log a warning, or do 106 nothing, respectively, if a declaration is violated. 107 108 Args: 109 max_positional_arguments: Maximum number of positional arguments. All 110 parameters after the this index must be keyword only. 111 112 Returns: 113 A decorator that prevents using arguments after max_positional_args from 114 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 util.positional_parameters_enforcement is set to 119 POSITIONAL_EXCEPTION. 120 121 """ 122 def positional_decorator(wrapped): 123 @functools.wraps(wrapped) 124 def positional_wrapper(*args, **kwargs): 125 if len(args) > max_positional_args: 126 plural_s = '' 127 if max_positional_args != 1: 128 plural_s = 's' 129 message = '%s() takes at most %d positional argument%s (%d given)' % ( 130 wrapped.__name__, max_positional_args, plural_s, len(args)) 131 if positional_parameters_enforcement == POSITIONAL_EXCEPTION: 132 raise TypeError(message) 133 elif positional_parameters_enforcement == POSITIONAL_WARNING: 134 logger.warning(message) 135 else: # IGNORE 136 pass 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 dict_to_tuple_key(dictionary): 167 """Converts a dictionary to a tuple that can be used as an immutable key. 168 169 The resulting key is always sorted so that logically equivalent dictionaries 170 always produce an identical tuple for a key. 171 172 Args: 173 dictionary: the dictionary to use as the key. 174 175 Returns: 176 A tuple representing the dictionary in it's naturally sorted ordering. 177 """ 178 return tuple(sorted(dictionary.items())) 179 180 181def _add_query_parameter(url, name, value): 182 """Adds a query parameter to a url. 183 184 Replaces the current value if it already exists in the URL. 185 186 Args: 187 url: string, url to add the query parameter to. 188 name: string, query parameter name. 189 value: string, query parameter value. 190 191 Returns: 192 Updated query parameter. Does not update the url if value is None. 193 """ 194 if value is None: 195 return url 196 else: 197 parsed = list(urllib.parse.urlparse(url)) 198 q = dict(urllib.parse.parse_qsl(parsed[4])) 199 q[name] = value 200 parsed[4] = urllib.parse.urlencode(q) 201 return urllib.parse.urlunparse(parsed) 202