• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# 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})
205