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