customfunction.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. #!/usr/bin/env python
  2. """An object representing a custom EE Function."""
  3. # Using lowercase function naming to match the JavaScript names.
  4. # pylint: disable=g-bad-name
  5. # pylint: disable=g-bad-import-order
  6. from . import computedobject
  7. from . import ee_exception
  8. from . import ee_types
  9. from . import encodable
  10. from . import function
  11. from . import serializer
  12. # Multiple inheritance, yay! This is necessary because a CustomFunction needs to
  13. # know how to encode itself in different ways:
  14. # - as an Encodable: encode its definition
  15. # - as a Function: encode its invocation (which may also involve encoding its
  16. # definition, if that hasn't happened yet).
  17. class CustomFunction(function.Function, encodable.Encodable):
  18. """An object representing a custom EE Function."""
  19. def __init__(self, signature, body):
  20. """Creates a function defined by a given expression with unbound variables.
  21. The expression is created by evaluating the given function
  22. using variables as placeholders.
  23. Args:
  24. signature: The function signature. If any of the argument names are
  25. null, their names will be generated deterministically, based on
  26. the body.
  27. body: The Python function to evaluate.
  28. """
  29. variables = [CustomFunction.variable(arg['type'], arg['name'])
  30. for arg in signature['args']]
  31. if body(*variables) is None:
  32. raise ee_exception.EEException('User-defined methods must return a value')
  33. # The signature of the function.
  34. self._signature = CustomFunction._resolveNamelessArgs(
  35. signature, variables, body)
  36. # The expression to evaluate.
  37. self._body = body(*variables)
  38. def encode(self, encoder):
  39. return {
  40. 'type': 'Function',
  41. 'argumentNames': [x['name'] for x in self._signature['args']],
  42. 'body': encoder(self._body)
  43. }
  44. def encode_cloud_value(self, encoder):
  45. return {
  46. 'functionDefinitionValue': {
  47. 'argumentNames': [x['name'] for x in self._signature['args']],
  48. 'body': encoder(self._body)
  49. }
  50. }
  51. def encode_invocation(self, encoder):
  52. return self.encode(encoder)
  53. def encode_cloud_invocation(self, encoder):
  54. return {'functionReference': encoder(self)}
  55. def getSignature(self):
  56. """Returns a description of the interface provided by this function."""
  57. return self._signature
  58. @staticmethod
  59. def variable(type_name, name):
  60. """Returns a placeholder variable with a given name and EE type.
  61. Args:
  62. type_name: A class to mimic.
  63. name: The name of the variable as it will appear in the
  64. arguments of the custom functions that use this variable. If null,
  65. a name will be auto-generated in _resolveNamelessArgs().
  66. Returns:
  67. A variable with the given name implementing the given type.
  68. """
  69. var_type = ee_types.nameToClass(type_name) or computedobject.ComputedObject
  70. result = var_type.__new__(var_type)
  71. result.func = None
  72. result.args = None
  73. result.varName = name
  74. return result
  75. @staticmethod
  76. def create(func, return_type, arg_types):
  77. """Creates a CustomFunction.
  78. The result calls a given native function with the specified return type and
  79. argument types and auto-generated argument names.
  80. Args:
  81. func: The native function to wrap.
  82. return_type: The type of the return value, either as a string or a
  83. class reference.
  84. arg_types: The types of the arguments, either as strings or class
  85. references.
  86. Returns:
  87. The constructed CustomFunction.
  88. """
  89. def StringifyType(t):
  90. return t if isinstance(t, str) else ee_types.classToName(t)
  91. args = [{'name': None, 'type': StringifyType(i)} for i in arg_types]
  92. signature = {
  93. 'name': '',
  94. 'returns': StringifyType(return_type),
  95. 'args': args
  96. }
  97. return CustomFunction(signature, func)
  98. @staticmethod
  99. def _resolveNamelessArgs(signature, variables, body):
  100. """Deterministically generates names for unnamed variables.
  101. The names are based on the body of the function.
  102. Args:
  103. signature: The signature which may contain null argument names.
  104. variables: A list of variables, some of which may be nameless.
  105. These will be updated to include names when this method returns.
  106. body: The Python function to evaluate.
  107. Returns:
  108. The signature with null arg names resolved.
  109. """
  110. nameless_arg_indices = []
  111. for i, variable in enumerate(variables):
  112. if variable.varName is None:
  113. nameless_arg_indices.append(i)
  114. # Do we have any nameless arguments at all?
  115. if not nameless_arg_indices:
  116. return signature
  117. # Generate the name base by counting the number of custom functions
  118. # within the body.
  119. def CountFunctions(expression):
  120. """Counts the number of custom functions in a serialized expression."""
  121. def CountNodes(nodes):
  122. return sum([CountNode(node) for node in nodes])
  123. def CountNode(node):
  124. if 'functionDefinitionValue' in node:
  125. return 1
  126. elif 'arrayValue' in node:
  127. return CountNodes(node['arrayValue']['values'])
  128. elif 'dictionaryValue' in node:
  129. return CountNodes(node['dictionaryValue']['values'].values())
  130. elif 'functionInvocationValue' in node:
  131. fn = node['functionInvocationValue']
  132. return CountNodes(fn['arguments'].values())
  133. return 0
  134. return CountNodes(expression['values'].values())
  135. # There are three function building phases, which each call body():
  136. # 1 - Check Return. The constructor verifies that body() returns a result,
  137. # but does not try to serialize the result. If the function tries to use
  138. # unbound variables (eg, using .getInfo() or print()), ComputedObject will
  139. # throw an exception when these calls try to serialize themselves, so that
  140. # unbound variables are not passed in server calls.
  141. # 2 - Count Functions. We serialize the result here. At this point all
  142. # variables must have names for serialization to succeed, but we don't yet
  143. # know the correct function depth. So we serialize with unbound_name set to
  144. # '<unbound>', which should silently succeed. If this does end up in server
  145. # calls, the function is very unusual: the first call doesn't use unbound
  146. # variables but the second call does. In this rare case we will return
  147. # server errors complaining about <unbound>.
  148. # 3 - Final Serialize. Finally, the constructor calls body() with the
  149. # correct, depth-dependent names, which are used when the CustomFunction
  150. # is serialized and sent to the server.
  151. serialized_body = serializer.encode(
  152. body(*variables), for_cloud_api=True, unbound_name='<unbound>')
  153. base_name = '_MAPPING_VAR_%d_' % CountFunctions(serialized_body)
  154. # Update the vars and signature by the name.
  155. for (i, index) in enumerate(nameless_arg_indices):
  156. name = base_name + str(i)
  157. variables[index].varName = name
  158. signature['args'][index]['name'] = name
  159. return signature