Skip to content

Commit fc13ba2

Browse files
tetrominocopybara-github
authored andcommitted
Custom init callback for providers
Implements #14392 RELNOTES: provider() has a new parameter: init, a callback for performing pre-processing and validation of field values. Iff this parameter is set, provider() returns a tuple of 2 elements: the usual provider symbol (which, when called, invokes init) and a raw constructor (which bypasses init). PiperOrigin-RevId: 422702617
1 parent b9ba990 commit fc13ba2

File tree

8 files changed

+657
-40
lines changed

8 files changed

+657
-40
lines changed

site/docs/skylark/rules.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,89 @@ def _example_library_impl(ctx):
570570
]
571571
```
572572

573+
##### Custom initialization of providers
574+
575+
It's possible to guard the instantiation of a provider with custom
576+
preprocessing and validation logic. This can be used to ensure that all
577+
provider instances obey certain invariants, or to give users a cleaner API for
578+
obtaining an instance.
579+
580+
This is done by passing an `init` callback to the
581+
[`provider`](lib/globals.html#provider) function. If this callback is given, the
582+
return type of `provider()` changes to be a tuple of two values: the provider
583+
symbol that is the ordinary return value when `init` is not used, and a "raw
584+
constructor".
585+
586+
In this case, when the provider symbol is called, instead of directly returning
587+
a new instance, it will forward the arguments along to the `init` callback. The
588+
callback's return value must be a dict mapping field names (strings) to values;
589+
this is used to initialize the fields of the new instance. Note that the
590+
callback may have any signature, and if the arguments do not match the signature
591+
an error is reported as if the callback were invoked directly.
592+
593+
The raw constructor, by contrast, will bypass the `init` callback.
594+
595+
The following example uses `init` to preprocess and validate its arguments:
596+
597+
```python
598+
# //pkg:exampleinfo.bzl
599+
600+
_core_headers = [...] # private constant representing standard library files
601+
602+
# It's possible to define an init accepting positional arguments, but
603+
# keyword-only arguments are preferred.
604+
def _exampleinfo_init(*, files_to_link, headers = None, allow_empty_files_to_link = False):
605+
if not files_to_link and not allow_empty_files_to_link:
606+
fail("files_to_link may not be empty")
607+
all_headers = depset(_core_headers, transitive = headers)
608+
return {'files_to_link': files_to_link, 'headers': all_headers}
609+
610+
ExampleInfo, _new_exampleinfo = provider(
611+
...
612+
init = _exampleinfo_init)
613+
614+
export ExampleInfo
615+
```
616+
617+
A rule implementation may then instantiate the provider as follows:
618+
619+
```python
620+
ExampleInfo(
621+
files_to_link=my_files_to_link, # may not be empty
622+
headers = my_headers, # will automatically include the core headers
623+
)
624+
```
625+
626+
The raw constructor can be used to define alternative public factory functions
627+
that do not go through the `init` logic. For example, in exampleinfo.bzl we
628+
could define:
629+
630+
```python
631+
def make_barebones_exampleinfo(headers):
632+
"""Returns an ExampleInfo with no files_to_link and only the specified headers."""
633+
return _new_exampleinfo(files_to_link = depset(), headers = all_headers)
634+
```
635+
636+
Typically, the raw constructor is bound to a variable whose name begins with an
637+
underscore (`_new_exampleinfo` above), so that user code cannot load it and
638+
generate arbitrary provider instances.
639+
640+
Another use for `init` is to simply prevent the user from calling the provider
641+
symbol altogether, and force them to use a factory function instead:
642+
643+
```python
644+
def _exampleinfo_init_banned(*args, **kwargs):
645+
fail("Do not call ExampleInfo(). Use make_exampleinfo() instead.")
646+
647+
ExampleInfo, _new_exampleinfo = provider(
648+
...
649+
init = _exampleinfo_init_banned)
650+
651+
def make_exampleinfo(...):
652+
...
653+
return _new_exampleinfo(...)
654+
```
655+
573656
<a name="executable-rules"></a>
574657

575658
## Executable rules and test rules

src/main/java/com/google/devtools/build/lib/analysis/starlark/StarlarkRuleClassFunctions.java

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@
7777
import com.google.devtools.build.lib.packages.Package.NameConflictException;
7878
import com.google.devtools.build.lib.packages.PackageFactory.PackageContext;
7979
import com.google.devtools.build.lib.packages.PredicateWithMessage;
80-
import com.google.devtools.build.lib.packages.Provider;
8180
import com.google.devtools.build.lib.packages.RuleClass;
8281
import com.google.devtools.build.lib.packages.RuleClass.Builder.RuleClassType;
8382
import com.google.devtools.build.lib.packages.RuleClass.ToolchainTransitionMode;
@@ -272,14 +271,25 @@ public static RuleClass getTestBaseRule(RuleDefinitionEnvironment env) {
272271
}
273272

274273
@Override
275-
public Provider provider(String doc, Object fields, StarlarkThread thread) throws EvalException {
274+
public Object provider(String doc, Object fields, Object init, StarlarkThread thread)
275+
throws EvalException {
276276
StarlarkProvider.Builder builder = StarlarkProvider.builder(thread.getCallerLocation());
277277
if (fields instanceof Sequence) {
278278
builder.setSchema(Sequence.cast(fields, String.class, "fields"));
279279
} else if (fields instanceof Dict) {
280280
builder.setSchema(Dict.cast(fields, String.class, String.class, "fields").keySet());
281281
}
282-
return builder.build();
282+
if (init == Starlark.NONE) {
283+
return builder.build();
284+
} else {
285+
if (init instanceof StarlarkCallable) {
286+
builder.setInit((StarlarkCallable) init);
287+
} else {
288+
throw Starlark.errorf("got %s for init, want callable value", Starlark.type(init));
289+
}
290+
StarlarkProvider provider = builder.build();
291+
return Tuple.of(provider, provider.createRawConstructor());
292+
}
283293
}
284294

285295
// TODO(bazel-team): implement attribute copy and other rule properties

src/main/java/com/google/devtools/build/lib/packages/StarlarkInfo.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,9 +126,9 @@ static StarlarkInfo createFromNamedArgs(
126126
List<String> unexpected = unexpectedKeys(schema, table, n);
127127
if (unexpected != null) {
128128
throw Starlark.errorf(
129-
"unexpected keyword%s %s in call to instantiate provider %s",
129+
"got unexpected field%s '%s' in call to instantiate provider %s",
130130
unexpected.size() > 1 ? "s" : "",
131-
Joiner.on(", ").join(unexpected),
131+
Joiner.on("', '").join(unexpected),
132132
provider.getPrintableName());
133133
}
134134
}

src/main/java/com/google/devtools/build/lib/packages/StarlarkProvider.java

Lines changed: 107 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,17 @@
1414

1515
package com.google.devtools.build.lib.packages;
1616

17+
import com.google.common.annotations.VisibleForTesting;
1718
import com.google.common.base.Preconditions;
1819
import com.google.common.collect.ImmutableList;
1920
import com.google.devtools.build.lib.cmdline.Label;
2021
import com.google.devtools.build.lib.events.EventHandler;
2122
import com.google.devtools.build.lib.util.Fingerprint;
2223
import java.util.Collection;
24+
import java.util.Map;
2325
import java.util.Objects;
2426
import javax.annotation.Nullable;
27+
import net.starlark.java.eval.Dict;
2528
import net.starlark.java.eval.EvalException;
2629
import net.starlark.java.eval.Printer;
2730
import net.starlark.java.eval.Starlark;
@@ -39,6 +42,11 @@
3942
* providers can have any set of fields on them, whereas instances of schemaful providers may have
4043
* only the fields that are named in the schema.
4144
*
45+
* <p>{@code StarlarkProvider} may have a custom initializer callback, which might perform
46+
* preprocessing or validation of field values. This callback (if defined) is automatically invoked
47+
* when the provider is called. To create instances of the provider without calling the initializer
48+
* callback, use the callable returned by {@code StarlarkProvider#createRawConstructor}.
49+
*
4250
* <p>Exporting a {@code StarlarkProvider} creates a key that is used to uniquely identify it.
4351
* Usually a provider is exported by calling {@link #export}, but a test may wish to just create a
4452
* pre-exported provider directly. Exported providers use only their key for {@link #equals} and
@@ -53,6 +61,11 @@ public final class StarlarkProvider implements StarlarkCallable, StarlarkExporta
5361
// as it lets us verify table ⊆ schema in O(n) time without temporaries.
5462
@Nullable private final ImmutableList<String> schema;
5563

64+
// Optional custom initializer callback. If present, it is invoked with the same positional and
65+
// keyword arguments as were passed to the provider constructor. The return value must be a
66+
// Starlark dict mapping field names (string keys) to their values.
67+
@Nullable private final StarlarkCallable init;
68+
5669
/** Null iff this provider has not yet been exported. Mutated by {@link export}. */
5770
@Nullable private Key key;
5871

@@ -78,6 +91,8 @@ public static final class Builder {
7891

7992
@Nullable private ImmutableList<String> schema;
8093

94+
@Nullable private StarlarkCallable init;
95+
8196
@Nullable private Key key;
8297

8398
private Builder(Location location) {
@@ -93,6 +108,24 @@ public Builder setSchema(Collection<String> schema) {
93108
return this;
94109
}
95110

111+
/**
112+
* Sets the custom initializer callback for instances of the provider built by this builder.
113+
*
114+
* <p>The initializer callback will be automatically invoked when the provider is called. To
115+
* bypass the custom initializer callback, use the callable returned by {@link
116+
* StarlarkProvider#createRawConstructor}.
117+
*
118+
* @param init A callback that accepts the arguments passed to the provider constructor, and
119+
* which returns a dict mapping field names to their values. The resulting provider instance
120+
* is created as though the dict were passed as **kwargs to the raw constructor. In
121+
* particular, for a schemaful provider, the dict may not contain keys not listed in the
122+
* schema.
123+
*/
124+
public Builder setInit(StarlarkCallable init) {
125+
this.init = init;
126+
return this;
127+
}
128+
96129
/** Sets the provider built by this builder to be exported with the given key. */
97130
public Builder setExported(Key key) {
98131
this.key = key;
@@ -101,32 +134,102 @@ public Builder setExported(Key key) {
101134

102135
/** Builds a StarlarkProvider. */
103136
public StarlarkProvider build() {
104-
return new StarlarkProvider(location, schema, key);
137+
return new StarlarkProvider(location, schema, init, key);
105138
}
106139
}
107140

108141
/**
109142
* Constructs the provider.
110143
*
111-
* <p>If {@code key} is null, the provider is unexported. If {@code schema} is null, the provider
112-
* is schemaless.
144+
* <p>If {@code schema} is null, the provider is schemaless. If {@code init} is null, no custom
145+
* initializer callback will be used (i.e., calling the provider is the same as simply calling the
146+
* raw constructor). If {@code key} is null, the provider is unexported.
113147
*/
114148
private StarlarkProvider(
115-
Location location, @Nullable ImmutableList<String> schema, @Nullable Key key) {
149+
Location location,
150+
@Nullable ImmutableList<String> schema,
151+
@Nullable StarlarkCallable init,
152+
@Nullable Key key) {
116153
this.location = location;
117154
this.schema = schema;
155+
this.init = init;
118156
this.key = key;
119157
}
120158

159+
private static Object[] toNamedArgs(Object value, String descriptionForError)
160+
throws EvalException {
161+
Dict<String, Object> kwargs = Dict.cast(value, String.class, Object.class, descriptionForError);
162+
Object[] named = new Object[2 * kwargs.size()];
163+
int i = 0;
164+
for (Map.Entry<String, Object> e : kwargs.entrySet()) {
165+
named[i++] = e.getKey();
166+
named[i++] = e.getValue();
167+
}
168+
return named;
169+
}
170+
121171
@Override
122172
public Object fastcall(StarlarkThread thread, Object[] positional, Object[] named)
173+
throws InterruptedException, EvalException {
174+
if (init == null) {
175+
return fastcallRawConstructor(thread, positional, named);
176+
}
177+
178+
Object initResult = Starlark.fastcall(thread, init, positional, named);
179+
return StarlarkInfo.createFromNamedArgs(
180+
this,
181+
toNamedArgs(initResult, "return value of provider init()"),
182+
schema,
183+
thread.getCallerLocation());
184+
}
185+
186+
private Object fastcallRawConstructor(StarlarkThread thread, Object[] positional, Object[] named)
123187
throws EvalException {
124188
if (positional.length > 0) {
125189
throw Starlark.errorf("%s: unexpected positional arguments", getName());
126190
}
127191
return StarlarkInfo.createFromNamedArgs(this, named, schema, thread.getCallerLocation());
128192
}
129193

194+
private static final class RawConstructor implements StarlarkCallable {
195+
private final StarlarkProvider provider;
196+
197+
private RawConstructor(StarlarkProvider provider) {
198+
this.provider = provider;
199+
}
200+
201+
@Override
202+
public Object fastcall(StarlarkThread thread, Object[] positional, Object[] named)
203+
throws EvalException {
204+
return provider.fastcallRawConstructor(thread, positional, named);
205+
}
206+
207+
@Override
208+
public String getName() {
209+
StringBuilder name = new StringBuilder("<raw constructor");
210+
if (provider.isExported()) {
211+
name.append(" for ").append(provider.getName());
212+
}
213+
name.append(">");
214+
return name.toString();
215+
}
216+
217+
@Override
218+
public Location getLocation() {
219+
return provider.location;
220+
}
221+
}
222+
223+
public StarlarkCallable createRawConstructor() {
224+
return new RawConstructor(this);
225+
}
226+
227+
@Nullable
228+
@VisibleForTesting
229+
public StarlarkCallable getInit() {
230+
return init;
231+
}
232+
130233
@Override
131234
public Location getLocation() {
132235
return location;

0 commit comments

Comments
 (0)