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