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( 
 32      [POSITIONAL_WARNING, POSITIONAL_EXCEPTION, POSITIONAL_IGNORE] 
 33  ) 
 34   
 35  positional_parameters_enforcement = POSITIONAL_WARNING 
 36   
 37  _SYM_LINK_MESSAGE = "File: {0}: Is a symbolic link." 
 38  _IS_DIR_MESSAGE = "{0}: Is a directory" 
 39  _MISSING_FILE_MESSAGE = "Cannot access {0}: No such file or directory" 
 43      """A decorator to declare that only the first N arguments may be positional. 
 44   
 45      This decorator makes it easy to support Python 3 style keyword-only 
 46      parameters. For example, in Python 3 it is possible to write:: 
 47   
 48          def fn(pos1, *, kwonly1=None, kwonly1=None): 
 49              ... 
 50   
 51      All named parameters after ``*`` must be a keyword:: 
 52   
 53          fn(10, 'kw1', 'kw2')  # Raises exception. 
 54          fn(10, kwonly1='kw1')  # Ok. 
 55   
 56      Example 
 57      ^^^^^^^ 
 58   
 59      To define a function like above, do:: 
 60   
 61          @positional(1) 
 62          def fn(pos1, kwonly1=None, kwonly2=None): 
 63              ... 
 64   
 65      If no default value is provided to a keyword argument, it becomes a 
 66      required keyword argument:: 
 67   
 68          @positional(0) 
 69          def fn(required_kw): 
 70              ... 
 71   
 72      This must be called with the keyword parameter:: 
 73   
 74          fn()  # Raises exception. 
 75          fn(10)  # Raises exception. 
 76          fn(required_kw=10)  # Ok. 
 77   
 78      When defining instance or class methods always remember to account for 
 79      ``self`` and ``cls``:: 
 80   
 81          class MyClass(object): 
 82   
 83              @positional(2) 
 84              def my_method(self, pos1, kwonly1=None): 
 85                  ... 
 86   
 87              @classmethod 
 88              @positional(2) 
 89              def my_method(cls, pos1, kwonly1=None): 
 90                  ... 
 91   
 92      The positional decorator behavior is controlled by 
 93      ``_helpers.positional_parameters_enforcement``, which may be set to 
 94      ``POSITIONAL_EXCEPTION``, ``POSITIONAL_WARNING`` or 
 95      ``POSITIONAL_IGNORE`` to raise an exception, log a warning, or do 
 96      nothing, respectively, if a declaration is violated. 
 97   
 98      Args: 
 99          max_positional_arguments: Maximum number of positional arguments. All 
100                                    parameters after the this index must be 
101                                    keyword only. 
102   
103      Returns: 
104          A decorator that prevents using arguments after max_positional_args 
105          from being used as positional parameters. 
106   
107      Raises: 
108          TypeError: if a key-word only argument is provided as a positional 
109                     parameter, but only if 
110                     _helpers.positional_parameters_enforcement is set to 
111                     POSITIONAL_EXCEPTION. 
112      """ 
113   
114      def positional_decorator(wrapped): 
115          @functools.wraps(wrapped) 
116          def positional_wrapper(*args, **kwargs): 
117              if len(args) > max_positional_args: 
118                  plural_s = "" 
119                  if max_positional_args != 1: 
120                      plural_s = "s" 
121                  message = ( 
122                      "{function}() takes at most {args_max} positional " 
123                      "argument{plural} ({args_given} given)".format( 
124                          function=wrapped.__name__, 
125                          args_max=max_positional_args, 
126                          args_given=len(args), 
127                          plural=plural_s, 
128                      ) 
129                  ) 
130                  if positional_parameters_enforcement == POSITIONAL_EXCEPTION: 
131                      raise TypeError(message) 
132                  elif positional_parameters_enforcement == POSITIONAL_WARNING: 
133                      logger.warning(message) 
134              return wrapped(*args, **kwargs) 
 135   
136          return positional_wrapper 
137   
138      if isinstance(max_positional_args, six.integer_types): 
139          return positional_decorator 
140      else: 
141          args, _, _, defaults = inspect.getargspec(max_positional_args) 
142          return positional(len(args) - len(defaults))(max_positional_args) 
143   
146      """Parses unique key-value parameters from urlencoded content. 
147   
148      Args: 
149          content: string, URL-encoded key-value pairs. 
150   
151      Returns: 
152          dict, The key-value pairs from ``content``. 
153   
154      Raises: 
155          ValueError: if one of the keys is repeated. 
156      """ 
157      urlencoded_params = urllib.parse.parse_qs(content) 
158      params = {} 
159      for key, value in six.iteritems(urlencoded_params): 
160          if len(value) != 1: 
161              msg = "URL-encoded content contains a repeated value:" "%s -> %s" % ( 
162                  key, 
163                  ", ".join(value), 
164              ) 
165              raise ValueError(msg) 
166          params[key] = value[0] 
167      return params 
 168   
171      """Updates a URI with new query parameters. 
172   
173      If a given key from ``params`` is repeated in the ``uri``, then 
174      the URI will be considered invalid and an error will occur. 
175   
176      If the URI is valid, then each value from ``params`` will 
177      replace the corresponding value in the query parameters (if 
178      it exists). 
179   
180      Args: 
181          uri: string, A valid URI, with potential existing query parameters. 
182          params: dict, A dictionary of query parameters. 
183   
184      Returns: 
185          The same URI but with the new query parameters added. 
186      """ 
187      parts = urllib.parse.urlparse(uri) 
188      query_params = parse_unique_urlencoded(parts.query) 
189      query_params.update(params) 
190      new_query = urllib.parse.urlencode(query_params) 
191      new_parts = parts._replace(query=new_query) 
192      return urllib.parse.urlunparse(new_parts) 
 193   
196      """Adds a query parameter to a url. 
197   
198      Replaces the current value if it already exists in the URL. 
199   
200      Args: 
201          url: string, url to add the query parameter to. 
202          name: string, query parameter name. 
203          value: string, query parameter value. 
204   
205      Returns: 
206          Updated query parameter. Does not update the url if value is None. 
207      """ 
208      if value is None: 
209          return url 
210      else: 
211          return update_query_params(url, {name: value}) 
 212