# Copyright 2012 Google Inc. All Rights Reserved. """Handle dynamically loaded function signatures.""" # Using old-style python function naming on purpose to match the # javascript version's naming. # pylint: disable-msg=C6003,C6409 import json import keyword import numbers import textwrap import computedobject import data import ee_exception import feature import featurecollection import image import imagecollection import serializer # The list of function signatures. _signatures = None # The width of the generated method docstrings. DOCSTRING_WIDTH = 75 def init(): """Initialize the list of signatures from the Earth Engine frontend.""" global _signatures # pylint: disable-msg=W0603 if _signatures is None: _signatures = data.getAlgorithms() def getSignature(name): """Get a signature by name. This makes sure that the signatures have been initialized. Args: name: The name of the signature to get. Returns: The specified signature. """ init() signature = dict(_signatures[name]) signature['name'] = name return signature def allSignatures(): """Return all the signatures. This makes sure that the signatures have been initialized. Returns: All the signatures. """ init() return _signatures def _applySignature(signature, *args, **kwargs): """Call the given function with the specified named and positional args. Args: signature: The algorithm's signature. *args: The positional arguments to be passed to the algorithm. **kwargs: The named arguments to be passed to the algorithm. Returns: An object representing the called algorithm. If the signature specifies a recognized return type, the returned value will be wrapped in that type. Otherwise, returns just the JSON description of the algorithm invocation. Raises: ee_exception.EEException: if there's a problem matching up args with the signature. """ parameters = dict(kwargs) sigArgs = signature['args'] if len(sigArgs) < len(args): raise ee_exception.EEException('Incorrect number of arguments: ' + signature['name']) # Convert the positional arguments to named ones. for i in xrange(0, len(args)): name = sigArgs[i]['name'] if name in parameters: raise ee_exception.EEException('Argument already set: ' + signature['name'] + '(' + name + ')') else: parameters[name] = args[i] # Promote types. for i in xrange(0, len(sigArgs)): name = sigArgs[i]['name'] if name in parameters: parameters[name] = _promote(sigArgs[i]['type'], parameters[name]) # Check for unknown parameters. argNames = set([x['name'] for x in sigArgs]) unknown = set(parameters.keys()).difference(argNames) if unknown: raise ee_exception.EEException('Unknown arguments %s(%s): ' % (signature['name'], str(list(unknown)))) # Apply return type. parameters['algorithm'] = signature['name'] return _promote(signature['returns'], parameters) def _makeFunction(name, signature, is_instance, opt_bound_args=None): """Create a function for the given signature. Creates a function for the given signature, with an optional set of values to pre-bind to some of the function arguments. Args: name: The name of the function as it appears on the class. signature: The signature of the function. is_instance: Whether this creates an instance method by passing the object on which the function is being called as the first argument. opt_bound_args: A dictionary from arg names to values to pre-bind to this call. Returns: The bound function. """ opt_bound_args = opt_bound_args or {} def BoundFunction(*argsIn, **namedArgsIn): """A generated function for the given signature.""" argsIn = list(argsIn) if is_instance: argsIn[0] = argsIn[0]._description # pylint: disable-msg=protected-access named = dict(opt_bound_args) named.update(namedArgsIn) return _applySignature(signature, *argsIn, **named) BoundFunction.__name__ = name.encode('utf8') BoundFunction.__doc__ = _makeDoc(signature) if not is_instance: BoundFunction = staticmethod(BoundFunction) return BoundFunction def _makeAggregateFunction(name, signature, is_instance, opt_bound_args=None): """Create an aggregation function for the given signature. The aggregation function not only constructs the JSON for an algorithm call but actually runs it. The produced function accepts an optional callback as its last argument. Args: name: The name of the function as it appears on the class. signature: The signature of the function. is_instance: Whether this creates an instance method by passing the object on which the function is being called as the first argument. opt_bound_args: A dictionary from arg names to values to pre-bind to this call. Returns: The bound function. """ opt_bound_args = opt_bound_args or {} func = _makeFunction(name, signature, is_instance, opt_bound_args) def BoundFunction(*argsIn, **namedArgsIn): """A generated aggregation function for the given signature.""" description = func(*argsIn, **namedArgsIn) return data.getValue({'json': serializer.toJSON(description, False)}) BoundFunction.__name__ = func.__name__ BoundFunction.__doc__ = func.__doc__ if not is_instance: BoundFunction = staticmethod(BoundFunction) return BoundFunction def _makeMapFunction(name, signature, is_instance, opt_bound_args=None): """Create a mapping function. Creates a mapping function for the given signature, with an optional set of values to pre-bind to some of the function arguments. Args: name: The name of the function as it appears on the class. signature: The signature of the function. is_instance: If false, returns None. Kept to follow the same API as the rest of the make*Function functions. opt_bound_args: A dictionary from arg names to values to pre-bind to this call. Returns: The bound function. """ if not is_instance: return None opt_bound_args = opt_bound_args or {} def BoundFunction(target, *argsIn, **namedArgsIn): """A function generated for the given signature.""" # Don't use the first argument, and unset the return type. s = dict(signature) s['args'] = signature['args'][1:] s['returns'] = None named = dict(opt_bound_args) named.update(namedArgsIn) parameters = _applySignature(s, *argsIn, **named) # We convert to JSON and back so we can pop off the top-level algorithm. parameters = json.loads(parameters.serialize()) if 'algorithm' in parameters: parameters.pop('algorithm') description = { 'constantArgs': parameters, 'baseAlgorithm': signature['name'], 'collection': target, 'dynamicArgs': { signature['args'][0]['name']: '.all' }, 'algorithm': 'MapAlgorithm' } if signature['returns'] == 'Image': collectionClass = imagecollection.ImageCollection else: collectionClass = featurecollection.FeatureCollection # Mapping an algorithm that produces a value (e.g. area) attaches the # result to the objects instead of replacing them. if signature['returns'] not in ('Feature', 'EEObject'): description['destination'] = signature['name'].split('.')[-1] return collectionClass(description) BoundFunction.__name__ = name.encode('utf8') BoundFunction.__doc__ = ('Applies ' + signature['name'] + '() on each element in the collection.') return BoundFunction def _addFunctions(target, prefix, type_name, name_prefix='', wrapper=_makeFunction): """Add all the functions that begin with "prefix" to the target class. Args: target: The class to add to. prefix: The prefix to search for. type_name: The name of the object's type. Algorithms whose first argument matches this type as bound as instance methods, and those whose first argument doesn't match are bound as static methods. name_prefix: An optional string to prepend to the names of the added functions. wrapper: The function to use for converting a signature into a function. """ init() for name in _signatures: parts = name.split('.') if len(parts) == 2 and parts[0] == prefix: fname = name_prefix + parts[1] signature = _signatures[name] signature['name'] = name # Specifically handle the function names that are illegal in python. if keyword.iskeyword(fname): fname = fname.title() # Decide whether this is a static or an instance function. is_instance = (signature['args'] and _isSubtype(signature['args'][0]['type'], type_name)) # Don't overwrite existing versions of this function. if hasattr(target, fname): fname = '_' + fname method = wrapper(fname, signature, is_instance) if method: setattr(target, fname, method) def _makeDoc(signature, opt_bound_args=None): """Create a docstring for the given signature. Args: signature: The signature of the function. opt_bound_args: A list of names specifying the arguments that are bound before the call to the function is made. Returns: The docstring. """ opt_bound_args = opt_bound_args or [] parts = [] parts.append(textwrap.fill(signature['description'], width=DOCSTRING_WIDTH)) args = signature['args'] args = [i for i in args if i['name'] not in opt_bound_args] if args: parts.append('') parts.append('Args:') for arg in args: name_part = ' ' + arg['name'] + ': ' arg_doc = textwrap.fill(name_part + arg['description'], width=DOCSTRING_WIDTH - len(name_part), subsequent_indent=' ' * 6) parts.append(arg_doc) return u'\n'.join(parts).encode('utf8') def _isSubtype(firstType, secondType): """Checks whether a type is a subtype of another. Args: firstType: The first type name. secondType: The second type name. Returns: Whether secondType is a subtype of firstType. """ if firstType == secondType: return True elif firstType == 'EEObject': return secondType in ('Image', 'Feature', 'Collection', 'ImageCollection', 'FeatureCollection') elif firstType in ('FeatureCollection', 'EECollection', 'Collection'): return secondType in ('Collection', 'ImageCollection', 'FeatureCollection') elif firstType == 'Object': return True else: return False def _promote(klass, arg): """Wrap an argument in an object of the specified class. This is used to e.g.: promote numbers or strings to Images and arrays to Collections. Args: klass: The expected type. arg: The object to promote. Returns: The argument promoted if the class is recognized, otherwise the original argument. """ if klass == 'Image': return image.Image(arg) elif klass == 'ImageCollection': return imagecollection.ImageCollection(arg) elif klass in ('Feature', 'EEObject'): if isinstance(arg, (imagecollection.ImageCollection, featurecollection.FeatureCollection)): return feature.Feature({ 'type': 'Feature', 'geometry': arg.geometry(), 'properties': {} }) else: return feature.Feature(arg) elif klass in ('FeatureCollection', 'EECollection'): return featurecollection.FeatureCollection(arg) elif klass == 'ErrorMargin' and isinstance(arg, numbers.Number): return { 'type': 'ErrorMargin', 'unit': 'meters', 'value': arg } else: return computedobject.ComputedObject(arg) def variable(cls, name): # pylint: disable-msg=C6409,W0622 """Returns a variable with a given name that implements a given EE type. Args: cls: A type (class) to mimic. name: The name of the variable as it will appear in the arguments of the lambdas that use this variable. Returns: A placeholder with the specified name implementing the specified type. """ class Variable(cls): def __init__(self, name): self._description = { 'type': 'Variable', 'name': name } return Variable(name) def lambda_(args, body): # pylint: disable-msg=C6409,W0622 """Creates an EE lambda function. Args: args: The names of the arguments to the lambda. body: The expression to evaluate. Returns: An EE lambda object that can be used in place of algorithms. """ return { 'type': 'Algorithm', 'args': args, 'body': body }