Home | History | Annotate | Download | only in googleapiclient
      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 
     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'
     39 
     40 
     41 def 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 
    140 def 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 
    163 def 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 
    188 def _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