function.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. #!/usr/bin/env python
  2. """A base class for EE Functions."""
  3. # Using lowercase function naming to match the JavaScript names.
  4. # pylint: disable=g-bad-name
  5. import textwrap
  6. from . import computedobject
  7. from . import ee_exception
  8. from . import encodable
  9. from . import serializer
  10. class Function(encodable.EncodableFunction):
  11. """An abstract base class for functions callable by the EE API.
  12. Subclasses must implement encode_invocation() and getSignature().
  13. """
  14. # A function used to type-coerce arguments and return values.
  15. _promoter = staticmethod(lambda value, type_name: value)
  16. @staticmethod
  17. def _registerPromoter(promoter):
  18. """Registers a function used to type-coerce arguments and return values.
  19. Args:
  20. promoter: A function used to type-coerce arguments and return values.
  21. Passed a value as the first parameter and a type name as the second.
  22. Can be used, for example, promote numbers or strings to Images.
  23. Should return the input promoted if the type is recognized,
  24. otherwise the original input.
  25. """
  26. Function._promoter = staticmethod(promoter)
  27. def getSignature(self):
  28. """Returns a description of the interface provided by this function.
  29. Returns:
  30. The function's signature, a dictionary containing:
  31. name: string
  32. returns: type name string
  33. args: list of argument dictionaries, each containing:
  34. name: string
  35. type: type name string
  36. optional: boolean
  37. default: an arbitrary primitive or encodable object
  38. """
  39. raise NotImplementedError(
  40. 'Function subclasses must implement getSignature().')
  41. def call(self, *args, **kwargs):
  42. """Calls the function with the given positional and keyword arguments.
  43. Args:
  44. *args: The positional arguments to pass to the function.
  45. **kwargs: The named arguments to pass to the function.
  46. Returns:
  47. A ComputedObject representing the called function. If the signature
  48. specifies a recognized return type, the returned value will be cast
  49. to that type.
  50. """
  51. return self.apply(self.nameArgs(args, kwargs))
  52. def apply(self, named_args):
  53. """Calls the function with a dictionary of named arguments.
  54. Args:
  55. named_args: A dictionary of named arguments to pass to the function.
  56. Returns:
  57. A ComputedObject representing the called function. If the signature
  58. specifies a recognized return type, the returned value will be cast
  59. to that type.
  60. """
  61. result = computedobject.ComputedObject(self, self.promoteArgs(named_args))
  62. return Function._promoter(result, self.getReturnType())
  63. def promoteArgs(self, args):
  64. """Promotes arguments to their types based on the function's signature.
  65. Verifies that all required arguments are provided and no unknown arguments
  66. are present.
  67. Args:
  68. args: A dictionary of keyword arguments to the function.
  69. Returns:
  70. A dictionary of promoted arguments.
  71. Raises:
  72. EEException: If unrecognized arguments are passed or required ones are
  73. missing.
  74. """
  75. specs = self.getSignature()['args']
  76. # Promote all recognized args.
  77. promoted_args = {}
  78. known = set()
  79. for spec in specs:
  80. name = spec['name']
  81. if name in args:
  82. promoted_args[name] = Function._promoter(args[name], spec['type'])
  83. elif not spec.get('optional'):
  84. raise ee_exception.EEException(
  85. 'Required argument (%s) missing to function: %s'
  86. % (name, self.name))
  87. known.add(name)
  88. # Check for unknown arguments.
  89. unknown = set(args.keys()).difference(known)
  90. if unknown:
  91. raise ee_exception.EEException(
  92. 'Unrecognized arguments %s to function: %s' % (unknown, self.name))
  93. return promoted_args
  94. def nameArgs(self, args, extra_keyword_args=None):
  95. """Converts a list of positional arguments to a map of keyword arguments.
  96. Uses the function's signature for argument names. Note that this does not
  97. check whether the array contains enough arguments to satisfy the call.
  98. Args:
  99. args: Positional arguments to the function.
  100. extra_keyword_args: Optional named arguments to add.
  101. Returns:
  102. Keyword arguments to the function.
  103. Raises:
  104. EEException: If conflicting arguments or too many of them are supplied.
  105. """
  106. specs = self.getSignature()['args']
  107. # Handle positional arguments.
  108. if len(specs) < len(args):
  109. raise ee_exception.EEException(
  110. 'Too many (%d) arguments to function: %s' % (len(args), self.name))
  111. named_args = dict([(spec['name'], value)
  112. for spec, value in zip(specs, args)])
  113. # Handle keyword arguments.
  114. if extra_keyword_args:
  115. for name in extra_keyword_args:
  116. if name in named_args:
  117. raise ee_exception.EEException(
  118. 'Argument %s specified as both positional and '
  119. 'keyword to function: %s' % (name, self.name))
  120. named_args[name] = extra_keyword_args[name]
  121. # Unrecognized arguments are checked in promoteArgs().
  122. return named_args
  123. def getReturnType(self):
  124. return self.getSignature()['returns']
  125. def serialize(self, for_cloud_api=True):
  126. return serializer.toJSON(
  127. self, for_cloud_api=for_cloud_api
  128. )
  129. def __str__(self):
  130. """Returns a user-readable docstring for this function."""
  131. DOCSTRING_WIDTH = 75
  132. signature = self.getSignature()
  133. parts = []
  134. if 'description' in signature:
  135. parts.append(
  136. textwrap.fill(signature['description'], width=DOCSTRING_WIDTH))
  137. args = signature['args']
  138. if args:
  139. parts.append('')
  140. parts.append('Args:')
  141. for arg in args:
  142. name_part = ' ' + arg['name']
  143. if 'description' in arg:
  144. name_part += ': '
  145. arg_header = name_part + arg['description']
  146. else:
  147. arg_header = name_part
  148. arg_doc = textwrap.fill(arg_header,
  149. width=DOCSTRING_WIDTH - len(name_part),
  150. subsequent_indent=' ' * 6)
  151. parts.append(arg_doc)
  152. return '\n'.join(parts)
  153. class SecondOrderFunction(Function):
  154. """A function that executes the result of a function."""
  155. def __init__(self, function_body, signature):
  156. """Creates a SecondOrderFunction.
  157. Args:
  158. function_body: The function that returns the function to execute.
  159. signature: The signature of the function to execute, as described in
  160. getSignature().
  161. """
  162. super().__init__()
  163. self._function_body = function_body
  164. self._signature = signature
  165. def encode_invocation(self, encoder):
  166. return self._function_body.encode(encoder)
  167. def encode_cloud_invocation(self, encoder):
  168. return {'functionReference': encoder(self._function_body)}
  169. def getSignature(self):
  170. return self._signature