Skip to content

Commit d46b607

Browse files
dmaclachallevato
authored andcommitted
Add support for 'functools.partial' like functionality. (#34)
* Add support for 'functools.partial' like functionality. https://docs.python.org/3/library/functools.html#functools.partial
1 parent a5e23fd commit d46b607

File tree

7 files changed

+226
-0
lines changed

7 files changed

+226
-0
lines changed

BUILD

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ skylark_library(
2424
deps = [
2525
"//lib:collections",
2626
"//lib:dicts",
27+
"//lib:partial",
2728
"//lib:paths",
2829
"//lib:selects",
2930
"//lib:sets",

CONTRIBUTORS

+1
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ Nathan Herring <nherring@google.com>
1515
Laurent Le Brun <laurentlb@google.com>
1616
Dmitry Lomov <dslomov@google.com>
1717
Jingwen Chen <jingwen@google.com>
18+
Dave MacLachlan <dmaclach@google.com>

lib.bzl

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
load("//lib:collections.bzl", "collections")
1818
load("//lib:dicts.bzl", "dicts")
19+
load("//lib:partial.bzl", "partial")
1920
load("//lib:paths.bzl", "paths")
2021
load("//lib:selects.bzl", "selects")
2122
load("//lib:sets.bzl", "sets")

lib/BUILD

+5
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ skylark_library(
2121
srcs = ["dicts.bzl"],
2222
)
2323

24+
skylark_library(
25+
name = "partial",
26+
srcs = ["partial.bzl"],
27+
)
28+
2429
skylark_library(
2530
name = "paths",
2631
srcs = ["paths.bzl"],

lib/partial.bzl

+130
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
# Copyright 2018 The Bazel Authors. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Skylark module for working with function objects where some parameters are
16+
bound before the call.
17+
18+
Similar to https://docs.python.org/3/library/functools.html#functools.partial.
19+
"""
20+
21+
def _call(partial, *args, **kwargs):
22+
"""Calls a partial created using `make`.
23+
24+
Args:
25+
partial: The partial to be called.
26+
*args: Additional positional arguments to be appended to the ones given to
27+
make.
28+
**kwargs: Additional keyword arguments to augment and override the ones
29+
given to make.
30+
31+
Returns:
32+
Whatever the function in the partial returns.
33+
"""
34+
function_args = partial.args + args
35+
function_kwargs = dict(partial.kwargs)
36+
function_kwargs.update(kwargs)
37+
return partial.function(*function_args, **function_kwargs)
38+
39+
def _make(func, *args, **kwargs):
40+
"""Creates a partial that can be called using `call`.
41+
42+
A partial can have args assigned to it at the make site, and can have args
43+
passed to it at the call sites.
44+
45+
A partial 'function' can be defined with positional args and kwargs:
46+
47+
# function with no args
48+
def function1():
49+
...
50+
51+
# function with 2 args
52+
def function2(arg1, arg2):
53+
...
54+
55+
# function with 2 args and keyword args
56+
def function3(arg1, arg2, x, y):
57+
...
58+
59+
The positional args passed to the function are the args passed into make
60+
followed by any additional positional args given to call. The below example
61+
illustrates a function with two positional arguments where one is supplied by
62+
make and the other by call:
63+
64+
# function demonstrating 1 arg at make site, and 1 arg at call site
65+
def _foo(make_arg1, func_arg1):
66+
print(make_arg1 + " " + func_arg1 + "!")
67+
68+
For example:
69+
70+
hi_func = partial.make(_foo, "Hello")
71+
bye_func = partial.make(_foo, "Goodbye")
72+
partial.call(hi_func, "Jennifer")
73+
partial.call(hi_func, "Dave")
74+
partial.call(bye_func, "Jennifer")
75+
partial.call(bye_func, "Dave")
76+
77+
prints:
78+
79+
"Hello, Jennifer!"
80+
"Hello, Dave!"
81+
"Goodbye, Jennifer!"
82+
"Goodbye, Dave!"
83+
84+
The keyword args given to the function are the kwargs passed into make
85+
unioned with the keyword args given to call. In case of a conflict, the
86+
keyword args given to call take precedence. This allows you to set a default
87+
value for keyword arguments and override it at the call site.
88+
89+
Example with a make site arg, a call site arg, a make site kwarg and a
90+
call site kwarg:
91+
92+
def _foo(make_arg1, call_arg1, make_location, call_location):
93+
print(make_arg1 + " is from " + make_location + " and " +
94+
call_arg1 + " is from " + call_location + "!")
95+
96+
func = partial.make(_foo, "Ben", make_location="Hollywood")
97+
partial.call(func, "Jennifer", call_location="Denver")
98+
99+
Prints "Ben is from Hollywood and Jennifer is from Denver!".
100+
101+
partial.call(func, "Jennifer", make_location="LA", call_location="Denver")
102+
103+
Prints "Ben is from LA and Jennifer is from Denver!".
104+
105+
Note that keyword args may not overlap with positional args, regardless of
106+
whether they are given during the make or call step. For instance, you can't
107+
do:
108+
109+
def foo(x):
110+
pass
111+
112+
func = partial.make(foo, 1)
113+
partial.call(func, x=2)
114+
115+
116+
Args:
117+
func: The function to be called.
118+
*args: Positional arguments to be passed to function.
119+
**kwargs: Keyword arguments to be passed to function. Note that these can
120+
be overridden at the call sites.
121+
122+
Returns:
123+
A new `partial` that can be called using `call`
124+
"""
125+
return struct(function=func, args=args, kwargs=kwargs)
126+
127+
partial = struct(
128+
make=_make,
129+
call=_call,
130+
)

tests/BUILD

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
load(":collections_tests.bzl", "collections_test_suite")
22
load(":dicts_tests.bzl", "dicts_test_suite")
3+
load(":partial_tests.bzl", "partial_test_suite")
34
load(":paths_tests.bzl", "paths_test_suite")
45
load(":selects_tests.bzl", "selects_test_suite")
56
load(":sets_tests.bzl", "sets_test_suite")
@@ -13,6 +14,8 @@ collections_test_suite()
1314

1415
dicts_test_suite()
1516

17+
partial_test_suite()
18+
1619
paths_test_suite()
1720

1821
selects_test_suite()

tests/partial_tests.bzl

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# Copyright 2018 The Bazel Authors. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Unit tests for partial.bzl."""
16+
17+
load("//:lib.bzl", "partial", "asserts", "unittest")
18+
19+
20+
def _make_noargs_nokwargs():
21+
"""Test utility for no args no kwargs case"""
22+
return 1
23+
24+
def _make_args_nokwargs(arg1, arg2, arg3):
25+
"""Test utility for args no kwargs case"""
26+
return arg1 + arg2 + arg3
27+
28+
def _make_args_kwargs(arg1, arg2, arg3, **kwargs):
29+
"""Test utility for args and kwargs case"""
30+
return arg1 + arg2 + arg3 + kwargs["x"] + kwargs["y"]
31+
32+
def _call_noargs_nokwargs(call_arg1):
33+
"""Test utility no args no kwargs case where values passed from call site"""
34+
return call_arg1;
35+
36+
def _call_args_nokwargs(func_arg1, call_arg1):
37+
"""Test utility for args no kwargs case where values passed from call site"""
38+
return func_arg1 + call_arg1;
39+
40+
def _call_args_kwargs(func_arg1, call_arg1, func_mult, call_mult):
41+
"""Test utility for args and kwargs case where values passed from call site"""
42+
return (func_arg1 + call_arg1) * func_mult * call_mult;
43+
44+
def _make_call_test(ctx):
45+
"""Unit tests for partial.make and partial.call."""
46+
env = unittest.begin(ctx)
47+
48+
# Test cases where there are no args (or kwargs) at the make site, only
49+
# at the call site.
50+
foo = partial.make(_make_noargs_nokwargs)
51+
asserts.equals(env, 1, partial.call(foo))
52+
53+
foo = partial.make(_make_args_nokwargs)
54+
asserts.equals(env, 6, partial.call(foo, 1, 2, 3))
55+
56+
foo = partial.make(_make_args_kwargs)
57+
asserts.equals(env, 15, partial.call(foo, 1, 2, 3, x=4, y=5))
58+
59+
# Test cases where there are args (and/or kwargs) at the make site and the
60+
# call site.
61+
foo = partial.make(_call_noargs_nokwargs, 100)
62+
asserts.equals(env, 100, partial.call(foo))
63+
64+
foo = partial.make(_call_args_nokwargs, 100)
65+
asserts.equals(env, 112, partial.call(foo, 12))
66+
67+
foo = partial.make(_call_args_kwargs, 100, func_mult=10)
68+
asserts.equals(env, 2240, partial.call(foo, 12, call_mult=2))
69+
70+
# Test case where there are args and kwargs ath the make site, and the call
71+
# site overrides some make site args.
72+
foo = partial.make(_call_args_kwargs, 100, func_mult=10)
73+
asserts.equals(env, 1120, partial.call(foo, 12, func_mult=5, call_mult=2))
74+
75+
unittest.end(env)
76+
77+
make_call_test = unittest.make(_make_call_test)
78+
79+
80+
def partial_test_suite():
81+
"""Creates the test targets and test suite for partial.bzl tests."""
82+
unittest.suite(
83+
"partial_tests",
84+
make_call_test,
85+
)

0 commit comments

Comments
 (0)