diff --git a/BUILD b/BUILD index babb74e..8f89a83 100644 --- a/BUILD +++ b/BUILD @@ -24,6 +24,7 @@ skylark_library( deps = [ "//lib:collections", "//lib:dicts", + "//lib:partial", "//lib:paths", "//lib:selects", "//lib:sets", diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 1ea747a..fed38b9 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -15,3 +15,4 @@ Nathan Herring Laurent Le Brun Dmitry Lomov Jingwen Chen +Dave MacLachlan diff --git a/lib.bzl b/lib.bzl index 8e8e6cb..cee289b 100644 --- a/lib.bzl +++ b/lib.bzl @@ -16,6 +16,7 @@ load("//lib:collections.bzl", "collections") load("//lib:dicts.bzl", "dicts") +load("//lib:partial.bzl", "partial") load("//lib:paths.bzl", "paths") load("//lib:selects.bzl", "selects") load("//lib:sets.bzl", "sets") diff --git a/lib/BUILD b/lib/BUILD index 3f4ba17..ce585fb 100644 --- a/lib/BUILD +++ b/lib/BUILD @@ -21,6 +21,11 @@ skylark_library( srcs = ["dicts.bzl"], ) +skylark_library( + name = "partial", + srcs = ["partial.bzl"], +) + skylark_library( name = "paths", srcs = ["paths.bzl"], diff --git a/lib/partial.bzl b/lib/partial.bzl new file mode 100644 index 0000000..001767f --- /dev/null +++ b/lib/partial.bzl @@ -0,0 +1,130 @@ +# Copyright 2018 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Skylark module for working with function objects where some parameters are + bound before the call. + +Similar to https://docs.python.org/3/library/functools.html#functools.partial. +""" + +def _call(partial, *args, **kwargs): + """Calls a partial created using `make`. + + Args: + partial: The partial to be called. + *args: Additional positional arguments to be appended to the ones given to + make. + **kwargs: Additional keyword arguments to augment and override the ones + given to make. + + Returns: + Whatever the function in the partial returns. + """ + function_args = partial.args + args + function_kwargs = dict(partial.kwargs) + function_kwargs.update(kwargs) + return partial.function(*function_args, **function_kwargs) + +def _make(func, *args, **kwargs): + """Creates a partial that can be called using `call`. + + A partial can have args assigned to it at the make site, and can have args + passed to it at the call sites. + + A partial 'function' can be defined with positional args and kwargs: + + # function with no args + def function1(): + ... + + # function with 2 args + def function2(arg1, arg2): + ... + + # function with 2 args and keyword args + def function3(arg1, arg2, x, y): + ... + + The positional args passed to the function are the args passed into make + followed by any additional positional args given to call. The below example + illustrates a function with two positional arguments where one is supplied by + make and the other by call: + + # function demonstrating 1 arg at make site, and 1 arg at call site + def _foo(make_arg1, func_arg1): + print(make_arg1 + " " + func_arg1 + "!") + + For example: + + hi_func = partial.make(_foo, "Hello") + bye_func = partial.make(_foo, "Goodbye") + partial.call(hi_func, "Jennifer") + partial.call(hi_func, "Dave") + partial.call(bye_func, "Jennifer") + partial.call(bye_func, "Dave") + + prints: + + "Hello, Jennifer!" + "Hello, Dave!" + "Goodbye, Jennifer!" + "Goodbye, Dave!" + + The keyword args given to the function are the kwargs passed into make + unioned with the keyword args given to call. In case of a conflict, the + keyword args given to call take precedence. This allows you to set a default + value for keyword arguments and override it at the call site. + + Example with a make site arg, a call site arg, a make site kwarg and a + call site kwarg: + + def _foo(make_arg1, call_arg1, make_location, call_location): + print(make_arg1 + " is from " + make_location + " and " + + call_arg1 + " is from " + call_location + "!") + + func = partial.make(_foo, "Ben", make_location="Hollywood") + partial.call(func, "Jennifer", call_location="Denver") + + Prints "Ben is from Hollywood and Jennifer is from Denver!". + + partial.call(func, "Jennifer", make_location="LA", call_location="Denver") + + Prints "Ben is from LA and Jennifer is from Denver!". + + Note that keyword args may not overlap with positional args, regardless of + whether they are given during the make or call step. For instance, you can't + do: + + def foo(x): + pass + + func = partial.make(foo, 1) + partial.call(func, x=2) + + + Args: + func: The function to be called. + *args: Positional arguments to be passed to function. + **kwargs: Keyword arguments to be passed to function. Note that these can + be overridden at the call sites. + + Returns: + A new `partial` that can be called using `call` + """ + return struct(function=func, args=args, kwargs=kwargs) + +partial = struct( + make=_make, + call=_call, +) diff --git a/tests/BUILD b/tests/BUILD index 9df7585..5dd8a68 100644 --- a/tests/BUILD +++ b/tests/BUILD @@ -1,5 +1,6 @@ load(":collections_tests.bzl", "collections_test_suite") load(":dicts_tests.bzl", "dicts_test_suite") +load(":partial_tests.bzl", "partial_test_suite") load(":paths_tests.bzl", "paths_test_suite") load(":selects_tests.bzl", "selects_test_suite") load(":sets_tests.bzl", "sets_test_suite") @@ -13,6 +14,8 @@ collections_test_suite() dicts_test_suite() +partial_test_suite() + paths_test_suite() selects_test_suite() diff --git a/tests/partial_tests.bzl b/tests/partial_tests.bzl new file mode 100644 index 0000000..6e4768b --- /dev/null +++ b/tests/partial_tests.bzl @@ -0,0 +1,85 @@ +# Copyright 2018 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Unit tests for partial.bzl.""" + +load("//:lib.bzl", "partial", "asserts", "unittest") + + +def _make_noargs_nokwargs(): + """Test utility for no args no kwargs case""" + return 1 + +def _make_args_nokwargs(arg1, arg2, arg3): + """Test utility for args no kwargs case""" + return arg1 + arg2 + arg3 + +def _make_args_kwargs(arg1, arg2, arg3, **kwargs): + """Test utility for args and kwargs case""" + return arg1 + arg2 + arg3 + kwargs["x"] + kwargs["y"] + +def _call_noargs_nokwargs(call_arg1): + """Test utility no args no kwargs case where values passed from call site""" + return call_arg1; + +def _call_args_nokwargs(func_arg1, call_arg1): + """Test utility for args no kwargs case where values passed from call site""" + return func_arg1 + call_arg1; + +def _call_args_kwargs(func_arg1, call_arg1, func_mult, call_mult): + """Test utility for args and kwargs case where values passed from call site""" + return (func_arg1 + call_arg1) * func_mult * call_mult; + +def _make_call_test(ctx): + """Unit tests for partial.make and partial.call.""" + env = unittest.begin(ctx) + + # Test cases where there are no args (or kwargs) at the make site, only + # at the call site. + foo = partial.make(_make_noargs_nokwargs) + asserts.equals(env, 1, partial.call(foo)) + + foo = partial.make(_make_args_nokwargs) + asserts.equals(env, 6, partial.call(foo, 1, 2, 3)) + + foo = partial.make(_make_args_kwargs) + asserts.equals(env, 15, partial.call(foo, 1, 2, 3, x=4, y=5)) + + # Test cases where there are args (and/or kwargs) at the make site and the + # call site. + foo = partial.make(_call_noargs_nokwargs, 100) + asserts.equals(env, 100, partial.call(foo)) + + foo = partial.make(_call_args_nokwargs, 100) + asserts.equals(env, 112, partial.call(foo, 12)) + + foo = partial.make(_call_args_kwargs, 100, func_mult=10) + asserts.equals(env, 2240, partial.call(foo, 12, call_mult=2)) + + # Test case where there are args and kwargs ath the make site, and the call + # site overrides some make site args. + foo = partial.make(_call_args_kwargs, 100, func_mult=10) + asserts.equals(env, 1120, partial.call(foo, 12, func_mult=5, call_mult=2)) + + unittest.end(env) + +make_call_test = unittest.make(_make_call_test) + + +def partial_test_suite(): + """Creates the test targets and test suite for partial.bzl tests.""" + unittest.suite( + "partial_tests", + make_call_test, + )