_helpers.py 6.6 KB

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