| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196 |
- #!/usr/bin/env python
- """An object representing a custom EE Function."""
- # Using lowercase function naming to match the JavaScript names.
- # pylint: disable=g-bad-name
- # pylint: disable=g-bad-import-order
- from . import computedobject
- from . import ee_exception
- from . import ee_types
- from . import encodable
- from . import function
- from . import serializer
- # Multiple inheritance, yay! This is necessary because a CustomFunction needs to
- # know how to encode itself in different ways:
- # - as an Encodable: encode its definition
- # - as a Function: encode its invocation (which may also involve encoding its
- # definition, if that hasn't happened yet).
- class CustomFunction(function.Function, encodable.Encodable):
- """An object representing a custom EE Function."""
- def __init__(self, signature, body):
- """Creates a function defined by a given expression with unbound variables.
- The expression is created by evaluating the given function
- using variables as placeholders.
- Args:
- signature: The function signature. If any of the argument names are
- null, their names will be generated deterministically, based on
- the body.
- body: The Python function to evaluate.
- """
- variables = [CustomFunction.variable(arg['type'], arg['name'])
- for arg in signature['args']]
- if body(*variables) is None:
- raise ee_exception.EEException('User-defined methods must return a value')
- # The signature of the function.
- self._signature = CustomFunction._resolveNamelessArgs(
- signature, variables, body)
- # The expression to evaluate.
- self._body = body(*variables)
- def encode(self, encoder):
- return {
- 'type': 'Function',
- 'argumentNames': [x['name'] for x in self._signature['args']],
- 'body': encoder(self._body)
- }
- def encode_cloud_value(self, encoder):
- return {
- 'functionDefinitionValue': {
- 'argumentNames': [x['name'] for x in self._signature['args']],
- 'body': encoder(self._body)
- }
- }
- def encode_invocation(self, encoder):
- return self.encode(encoder)
- def encode_cloud_invocation(self, encoder):
- return {'functionReference': encoder(self)}
- def getSignature(self):
- """Returns a description of the interface provided by this function."""
- return self._signature
- @staticmethod
- def variable(type_name, name):
- """Returns a placeholder variable with a given name and EE type.
- Args:
- type_name: A class to mimic.
- name: The name of the variable as it will appear in the
- arguments of the custom functions that use this variable. If null,
- a name will be auto-generated in _resolveNamelessArgs().
- Returns:
- A variable with the given name implementing the given type.
- """
- var_type = ee_types.nameToClass(type_name) or computedobject.ComputedObject
- result = var_type.__new__(var_type)
- result.func = None
- result.args = None
- result.varName = name
- return result
- @staticmethod
- def create(func, return_type, arg_types):
- """Creates a CustomFunction.
- The result calls a given native function with the specified return type and
- argument types and auto-generated argument names.
- Args:
- func: The native function to wrap.
- return_type: The type of the return value, either as a string or a
- class reference.
- arg_types: The types of the arguments, either as strings or class
- references.
- Returns:
- The constructed CustomFunction.
- """
- def StringifyType(t):
- return t if isinstance(t, str) else ee_types.classToName(t)
- args = [{'name': None, 'type': StringifyType(i)} for i in arg_types]
- signature = {
- 'name': '',
- 'returns': StringifyType(return_type),
- 'args': args
- }
- return CustomFunction(signature, func)
- @staticmethod
- def _resolveNamelessArgs(signature, variables, body):
- """Deterministically generates names for unnamed variables.
- The names are based on the body of the function.
- Args:
- signature: The signature which may contain null argument names.
- variables: A list of variables, some of which may be nameless.
- These will be updated to include names when this method returns.
- body: The Python function to evaluate.
- Returns:
- The signature with null arg names resolved.
- """
- nameless_arg_indices = []
- for i, variable in enumerate(variables):
- if variable.varName is None:
- nameless_arg_indices.append(i)
- # Do we have any nameless arguments at all?
- if not nameless_arg_indices:
- return signature
- # Generate the name base by counting the number of custom functions
- # within the body.
- def CountFunctions(expression):
- """Counts the number of custom functions in a serialized expression."""
- def CountNodes(nodes):
- return sum([CountNode(node) for node in nodes])
- def CountNode(node):
- if 'functionDefinitionValue' in node:
- return 1
- elif 'arrayValue' in node:
- return CountNodes(node['arrayValue']['values'])
- elif 'dictionaryValue' in node:
- return CountNodes(node['dictionaryValue']['values'].values())
- elif 'functionInvocationValue' in node:
- fn = node['functionInvocationValue']
- return CountNodes(fn['arguments'].values())
- return 0
- return CountNodes(expression['values'].values())
- # There are three function building phases, which each call body():
- # 1 - Check Return. The constructor verifies that body() returns a result,
- # but does not try to serialize the result. If the function tries to use
- # unbound variables (eg, using .getInfo() or print()), ComputedObject will
- # throw an exception when these calls try to serialize themselves, so that
- # unbound variables are not passed in server calls.
- # 2 - Count Functions. We serialize the result here. At this point all
- # variables must have names for serialization to succeed, but we don't yet
- # know the correct function depth. So we serialize with unbound_name set to
- # '<unbound>', which should silently succeed. If this does end up in server
- # calls, the function is very unusual: the first call doesn't use unbound
- # variables but the second call does. In this rare case we will return
- # server errors complaining about <unbound>.
- # 3 - Final Serialize. Finally, the constructor calls body() with the
- # correct, depth-dependent names, which are used when the CustomFunction
- # is serialized and sent to the server.
- serialized_body = serializer.encode(
- body(*variables), for_cloud_api=True, unbound_name='<unbound>')
- base_name = '_MAPPING_VAR_%d_' % CountFunctions(serialized_body)
- # Update the vars and signature by the name.
- for (i, index) in enumerate(nameless_arg_indices):
- name = base_name + str(i)
- variables[index].varName = name
- signature['args'][index]['name'] = name
- return signature
|