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([POSITIONAL_WARNING, POSITIONAL_EXCEPTION,
32 POSITIONAL_IGNORE])
33
34 positional_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'
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
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
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
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})
205