示例#1
0
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)
示例#2
0
    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")
示例#5
0
    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")
示例#6
0
    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,
        )
示例#7
0
    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
            )
        )