blob: 5e8184ba520c797e56c1ef3f5c797738ecf875ca [file] [log] [blame]
Helen Koikede13e3b2018-04-26 16:05:16 -03001# 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})