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