• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python
2#
3# Copyright 2014 Google Inc. All rights reserved.
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#     http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16#
17
18"""Common utility library."""
19
20import functools
21import inspect
22import logging
23
24import six
25from six.moves import urllib
26
27
28__author__ = [
29    'rafek@google.com (Rafe Kaplan)',
30    'guido@google.com (Guido van Rossum)',
31]
32
33__all__ = [
34    'positional',
35    'POSITIONAL_WARNING',
36    'POSITIONAL_EXCEPTION',
37    'POSITIONAL_IGNORE',
38]
39
40logger = logging.getLogger(__name__)
41
42POSITIONAL_WARNING = 'WARNING'
43POSITIONAL_EXCEPTION = 'EXCEPTION'
44POSITIONAL_IGNORE = 'IGNORE'
45POSITIONAL_SET = frozenset([POSITIONAL_WARNING, POSITIONAL_EXCEPTION,
46                            POSITIONAL_IGNORE])
47
48positional_parameters_enforcement = POSITIONAL_WARNING
49
50
51def positional(max_positional_args):
52    """A decorator to declare that only the first N arguments my be positional.
53
54    This decorator makes it easy to support Python 3 style keyword-only
55    parameters. For example, in Python 3 it is possible to write::
56
57        def fn(pos1, *, kwonly1=None, kwonly1=None):
58            ...
59
60    All named parameters after ``*`` must be a keyword::
61
62        fn(10, 'kw1', 'kw2')  # Raises exception.
63        fn(10, kwonly1='kw1')  # Ok.
64
65    Example
66    ^^^^^^^
67
68    To define a function like above, do::
69
70        @positional(1)
71        def fn(pos1, kwonly1=None, kwonly2=None):
72            ...
73
74    If no default value is provided to a keyword argument, it becomes a
75    required keyword argument::
76
77        @positional(0)
78        def fn(required_kw):
79            ...
80
81    This must be called with the keyword parameter::
82
83        fn()  # Raises exception.
84        fn(10)  # Raises exception.
85        fn(required_kw=10)  # Ok.
86
87    When defining instance or class methods always remember to account for
88    ``self`` and ``cls``::
89
90        class MyClass(object):
91
92            @positional(2)
93            def my_method(self, pos1, kwonly1=None):
94                ...
95
96            @classmethod
97            @positional(2)
98            def my_method(cls, pos1, kwonly1=None):
99                ...
100
101    The positional decorator behavior is controlled by
102    ``util.positional_parameters_enforcement``, which may be set to
103    ``POSITIONAL_EXCEPTION``, ``POSITIONAL_WARNING`` or
104    ``POSITIONAL_IGNORE`` to raise an exception, log a warning, or do
105    nothing, respectively, if a declaration is violated.
106
107    Args:
108        max_positional_arguments: Maximum number of positional arguments. All
109                                  parameters after the this index must be
110                                  keyword only.
111
112    Returns:
113        A decorator that prevents using arguments after max_positional_args
114        from being used as positional parameters.
115
116    Raises:
117        TypeError: if a key-word only argument is provided as a positional
118                   parameter, but only if
119                   util.positional_parameters_enforcement is set to
120                   POSITIONAL_EXCEPTION.
121    """
122
123    def positional_decorator(wrapped):
124        @functools.wraps(wrapped)
125        def positional_wrapper(*args, **kwargs):
126            if len(args) > max_positional_args:
127                plural_s = ''
128                if max_positional_args != 1:
129                    plural_s = 's'
130                message = ('%s() takes at most %d positional '
131                           'argument%s (%d given)' % (
132                               wrapped.__name__, max_positional_args,
133                               plural_s, len(args)))
134                if positional_parameters_enforcement == POSITIONAL_EXCEPTION:
135                    raise TypeError(message)
136                elif positional_parameters_enforcement == POSITIONAL_WARNING:
137                    logger.warning(message)
138                else:  # IGNORE
139                    pass
140            return wrapped(*args, **kwargs)
141        return positional_wrapper
142
143    if isinstance(max_positional_args, six.integer_types):
144        return positional_decorator
145    else:
146        args, _, _, defaults = inspect.getargspec(max_positional_args)
147        return positional(len(args) - len(defaults))(max_positional_args)
148
149
150def scopes_to_string(scopes):
151    """Converts scope value to a string.
152
153    If scopes is a string then it is simply passed through. If scopes is an
154    iterable then a string is returned that is all the individual scopes
155    concatenated with spaces.
156
157    Args:
158        scopes: string or iterable of strings, the scopes.
159
160    Returns:
161        The scopes formatted as a single string.
162    """
163    if isinstance(scopes, six.string_types):
164        return scopes
165    else:
166        return ' '.join(scopes)
167
168
169def string_to_scopes(scopes):
170    """Converts stringifed scope value to a list.
171
172    If scopes is a list then it is simply passed through. If scopes is an
173    string then a list of each individual scope is returned.
174
175    Args:
176        scopes: a string or iterable of strings, the scopes.
177
178    Returns:
179        The scopes in a list.
180    """
181    if not scopes:
182        return []
183    if isinstance(scopes, six.string_types):
184        return scopes.split(' ')
185    else:
186        return scopes
187
188
189def dict_to_tuple_key(dictionary):
190    """Converts a dictionary to a tuple that can be used as an immutable key.
191
192    The resulting key is always sorted so that logically equivalent
193    dictionaries always produce an identical tuple for a key.
194
195    Args:
196        dictionary: the dictionary to use as the key.
197
198    Returns:
199        A tuple representing the dictionary in it's naturally sorted ordering.
200    """
201    return tuple(sorted(dictionary.items()))
202
203
204def _add_query_parameter(url, name, value):
205    """Adds a query parameter to a url.
206
207    Replaces the current value if it already exists in the URL.
208
209    Args:
210        url: string, url to add the query parameter to.
211        name: string, query parameter name.
212        value: string, query parameter value.
213
214    Returns:
215        Updated query parameter. Does not update the url if value is None.
216    """
217    if value is None:
218        return url
219    else:
220        parsed = list(urllib.parse.urlparse(url))
221        q = dict(urllib.parse.parse_qsl(parsed[4]))
222        q[name] = value
223        parsed[4] = urllib.parse.urlencode(q)
224        return urllib.parse.urlunparse(parsed)
225