1   
  2   
  3   
  4   
  5   
  6   
  7   
  8   
  9   
 10   
 11   
 12   
 13   
 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' 
 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   
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   
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   
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