def ifelse(condition, true_value, false_value): """ An if-else statement: returns ``true_value`` if ``condition`` is True, otherwise ``false_value``. ``true_value`` and ``false_value`` must be the same type. (Note this is different from a Python if-else statement.) `ifelse` "short-circuits" like a Python conditional: only one of ``true_value`` or ``false_value`` will actually get computed. Note ---- Since Workflows objects cannot be used in Python ``if`` statements (their actual values aren't known until they're computed), the `ifelse` function lets you express conditional logic in Workflows operations. However, `ifelse` should be a last resort for large blocks of logic: in most cases, you can write code that is more efficient and easier to read using functionality like ``filter``, ``map``, :ref:`empty Image/ImageCollection handling <empty-rasters>`, ``pick_bands(allow_missing=True)``, `.Dict.get`, etc. Parameters ---------- condition: Bool The condition true_value: Value returned if ``condition`` is True. Must be the same type as ``false_value`` false_value: Value returned if ``condition`` is False. Must be the same type as ``true_value`` Returns ------- result: same as ``true_value`` and ``false_value`` ``true_value`` if ``condition`` is True, otherwise ``false_value`` Example ------- >>> import descarteslabs.workflows as wf >>> wf.ifelse(True, "yep!", "nope").inspect() # doctest: +SKIP "yep!" >>> wf.ifelse(False, "yep!", "nope").inspect() # doctest: +SKIP "nope" """ true_value = proxify(true_value) false_value = proxify(false_value) if type(true_value) is not type(false_value): raise TypeError( "Both cases of `ifelse` must be the same type. " "Got type {} for the true case, and type {} for the false case.". format(type(true_value).__name__, type(false_value).__name__)) first_guid = client.guid() delayed_true = client.function_graft(true_value, first_guid=first_guid) delayed_false = client.function_graft(false_value, first_guid=first_guid) return true_value._from_apply("wf.ifelse", condition, delayed_true, delayed_false)
def from_object(cls, obj): """ Turn a Workflows object that depends on parameters into a `Function`. Any parameters ``obj`` depends on become arguments to the `Function`. Calling that function essentially returns ``obj``, with the given values applied to those parameters. Example ------- >>> import descarteslabs.workflows as wf >>> word = wf.parameter("word", wf.Str) >>> repeats = wf.widgets.slider("repeats", min=0, max=5, step=1) >>> repeated = (word + " ") * repeats >>> # `repeated` depends on parameters; we have to pass values for them to compute it >>> repeated.inspect(word="foo", repeats=3) # doctest: +SKIP 'foo foo foo ' >>> # turn `repeated` into a Function that takes those parameters >>> repeat = wf.Function.from_object(repeated) >>> repeat <descarteslabs.workflows.types.function.function.Function[{'word': Str, 'repeats': Int}, Str] object at 0x...> >>> repeat("foo", 3).inspect() # doctest: +SKIP 'foo foo foo ' >>> repeat("hello", 2).inspect() # doctest: +SKIP 'hello hello ' Parameters ---------- obj: Proxytype A Workflows proxy object. Returns ------- func: Function A `Function` equivalent to ``obj`` TODO """ if any(p is obj for p in obj.params): raise ValueError( f"Cannot create a Function from a parameter object. This parameter {obj._name!r} " "is like an argument to a function---not the body of the function itself." ) named_args = { p._name: getattr(p, "_proxytype", type(p)) for p in obj.params } # ^ if any of the params are widgets (likely), use their base Proxytype in the Function type signature: # a Function[Checkbox, Slider, ...] would be 1) weird and 2) not serializeable. concrete_function_type = cls[named_args, type(obj)] graft = client.function_graft(obj, *(p.graft for p in obj.params)) # TODO we should probably store `obj.params` somewhere---that's valuable metadata maybe # to show the function as widgets, etc? return concrete_function_type._from_graft(graft)
def test_delay_fixedargs(self): result = [] def delayable(a, b): assert isinstance(a, Dict[Str, Int]) assert isinstance(b, Str) res = a[b] result.append(res) return res delayed = Function._delay(delayable, Int, Dict[Str, Int], Str) assert isinstance(delayed, Int) assert delayed.graft == client.function_graft(result[0], "a", "b")
def test_delay_anyargs(self): result = [] def delayable(a, b, c): assert isinstance(a, Any) assert isinstance(b, Any) assert isinstance(c, Any) res = a + b / c result.append(res) return res delayed = Function._delay(delayable, Int) assert isinstance(delayed, Int) assert delayed.graft == client.function_graft(result[0], "a", "b", "c")
def test_delay(self, returns): result = [] def delayable(x, a, b): assert isinstance(x, Float) assert isinstance(a, Dict[Str, Int]) assert isinstance(b, Str) res = a[b] result.append(res) return res delayed = Function._delay(delayable, returns, Float, b=Str, a=Dict[Str, Int]) assert isinstance(delayed, Int) assert delayed.graft == client.function_graft(result[0], "x", "a", "b")
def _delay(func, returns, *expected_arg_types, **expected_kwarg_types): """ Turn a Python function into a Proxytype object representing its logic. The logic of ``func`` is captured by passing dummy Proxytype objects through it (parameter references, cast to instances of ``argtypes``) and seeing what comes out the other end. Whatever operations ``func`` does on these arguments will be captured in their graft (or possibly cause an error, if invalid operations are done to the arguments), so the final value returned by ``func`` will have a graft representing equivalent logic to ``func``. Note that this won't work correctly if ``func`` uses control flow (conditionals), or is a non-pure function; i.e. calling ``func`` with the same arguments can produce different results. Closures (referencing names defined outside of ``func``) will work, but scope won't be quite captured correctly in the resulting graft, since those closed-over values will end up inside the scope of the function, instead of outside where they should be. Parameters ---------- func: callable Python callable. Must only take required positional arguments. returns: Proxytype or None The return value of the function is promoted to this type. If promotion fails, raises an error. If None, no promotion is attempted, and whatever ``func`` returned is returned from ``_delay``. *expected_arg_types: Proxytype Types of each positional argument to ``func``. An instance of each is passed into ``func``. *expected_kwarg_types: Proxytype Types of each named argument to ``func``. An instance of each is passed into ``func``. Returns ------- result: instance of ``returns`` A delayed-like object representing the logic of ``func``, with a graph that contains parameters """ if not callable(func): raise TypeError( "Expected a Python callable object to delay, not {!r}".format( func)) func_signature = signature(func) for name, param in func_signature.parameters.items(): if param.kind not in (param.POSITIONAL_ONLY, param.POSITIONAL_OR_KEYWORD): raise TypeError( "Workflows Functions only support positional arguments. " f"Parameter kind {param.kind!s}, used for {param} in the function " f"{func.__name__}, is unsupported.") if param.default is not param.empty: raise TypeError( f"Parameter {param} has a default value. Optional parameters " "(parameters with default values) are not supported in Workflows functions." ) try: # this will raise TypeError if the expected arguments # aren't compatible with the signature for `func` bound_expected_args = func_signature.bind( *expected_arg_types, **expected_kwarg_types).arguments except TypeError as e: expected_sig = _make_signature(expected_arg_types, expected_kwarg_types, returns) raise TypeError( "Your function takes the wrong parameters.\n" f"Expected signature: {expected_sig}\n" f"Your function's signature: {func_signature}.\n\n" f"When trying to call your function with those {len(expected_arg_types) + len(expected_kwarg_types)} " f"expected arguments, the specific error was: {e}") from None args = { name: identifier(name, type_) for name, type_ in bound_expected_args.items() } first_guid = client.guid() result = func(**args) if returns is not None: try: result = returns._promote(result) except ProxyTypeError as e: raise ProxyTypeError( "Cannot promote {} to {}, the expected return type of the function: {}" .format(result, returns.__name__, e)) else: result = proxify(result) return type(result)._from_graft( client.function_graft(result, *tuple(func_signature.parameters), first_guid=first_guid), params=result.params, )
def _delay(func, returns, *expected_arg_types): """ Turn a Python function into a Proxytype object representing its logic. The logic of ``func`` is captured by passing dummy Proxytype objects through it (parameter references, cast to instances of ``argtypes``) and seeing what comes out the other end. Whatever operations ``func`` does on these arguments will be captured in their graft (or possibly cause an error, if invalid operations are done to the arguments), so the final value returned by ``func`` will have a graft representing equivalent logic to ``func``. Note that this won't work correctly if ``func`` uses control flow (conditionals), or is a non-pure function; i.e. calling ``func`` with the same arguments can produce different results. Closures (referencing names defined outside of ``func``) will work, but scope won't be quite captured correctly in the resulting graft, since those closed-over values will end up inside the scope of the function, instead of outside where they should be. Parameters ---------- func: callable Python callable returns: Proxytype or None The return value of the function is promoted to this type. If promotion fails, raises an error. If None, no promotion is attempted, and whatever ``func`` returned is returned from ``_delay``. *expected_arg_types: Proxytype Types of each positional argument to ``func``. An instance of each is passed into ``func``. If none are given, ``func`` will be called with an instance of `Any` for each argument it takes. Returns ------- result: instance of ``returns`` A delayed-like object representing the logic of ``func``, with a graph that contains parameters """ if not callable(func): raise TypeError( "Expected a Python callable object to delay, not {!r}".format(func) ) func_signature = signature(func) if len(expected_arg_types) == 0: expected_arg_types = (Any,) * len(func_signature.parameters) # this will raise TypeError if the expected arguments # aren't compatible with the signature for `func` bound_expected_args = func_signature.bind(*expected_arg_types).arguments args = { name: identifier(name, type_) for name, type_ in six.iteritems(bound_expected_args) } first_guid = client.guid() result = func(**args) if returns is not None: try: result = returns._promote(result) except ProxyTypeError as e: raise ProxyTypeError( "Cannot promote {} to {}, the expected return type of the function: {}".format( result, returns.__name__, e ) ) else: result = proxify(result) return type(result)._from_graft( client.function_graft( result, *tuple(func_signature.parameters), first_guid=first_guid ) )