Skip to content

Commit a2cc32f

Browse files
authored
feat: EXPR and WRAP_ACTIONS v0 model parity via Rust bindings (#318)
Bring the pure-Python (v0) 2023-09 model to EXPR (RFC 0007) and WRAP_ACTIONS (RFC 0008) parity with openjd-rs, via the PyO3-bound Rust expression engine. Base of the RFC 0008 stack (openjd-sessions-for-python #333, openjd-cli #230). Supersedes #313 (production code contained here; its tests ported). - Deferred cancelation mode (FEATURE_BUNDLE_1): a format-string `mode` becomes CancelationMethodDeferred (mirrors CancelationMode::DeferredMode); the shape decision moves to run time so wrap hooks can forward "{{WrappedAction.Cancelation.Mode}}". - Typed whole-field resolution (RFC 0006): FormatString.resolve_value + typed_resolve_fields metadata; range: "{{Param.Values}}" with a LIST[*] parameter instantiates to the native list, evaluated exactly once per field. §3.4's 1024-value range cap enforced at job creation for expression-driven ranges (parity with openjd-rs ranges.rs). - Per-hook WRAP_ACTIONS variables via _template_field_inject (WrappedAction.*/WrappedEnv.Name/WrappedStep.Name visible only in their hook) and template-scope validation of timeout/cancelation via _template_field_scopes. - EXPR runtime plumbing: step-level `let` at job creation (extends_symtab, preserved on Step.let), Job.Name seeding (§7.3.1), URI-form PATH values, exported parse-memoized openjd.model.evaluate_let_bindings. - Performance: EXPR engine symbol table + host-context profile cached on SymbolTable keyed on a mutation version — 8.6 -> 1.9 ms per 20-arg action (removes the per-expression Rust-boundary rebuild). - Crates pinned to the published releases containing DeferredMode: openjd-expr 0.2.1, openjd-model 0.4.0, openjd-sessions 0.4.0. Validation tightenings (all matching openjd-rs and the spec; not purely additive): onEnter required without WRAP_ACTIONS (§3.5); 1024-value range caps (§3.4); 64-char identifier names without FEATURE_BUNDLE_1 (§7.1) and decimals-requires-SPIN_BOX (§2.4). Tested: 5394 passed / 24 skipped / 3 xfailed (coverage 94.4%); mypy, black, ruff, clippy -D warnings clean; conformance 2023-09 at 1154/1158 (remaining 4 need sessions-runtime eager validation, tracked separately); new §3.4/§7.3.1 conformance fixtures in openjd-specifications#155 validated against both implementations. Signed-off-by: David Leong <116610336+leongdl@users.noreply.github.com>
1 parent 29940e9 commit a2cc32f

39 files changed

Lines changed: 3818 additions & 440 deletions

Cargo.lock

Lines changed: 152 additions & 141 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
11
[workspace]
22
members = ["rust-bindings"]
33
resolver = "2"
4+
5+
# ── Local-development overrides (opt-in) ──────────────────────────────
6+
# Redirect the openjd-* crates to a sibling ~/openjd-rs checkout so the
7+
# bindings build against the in-flight Rust workspace instead of crates.io.
8+
# Cargo only honors [patch] at the workspace root, so it lives here rather
9+
# than in rust-bindings/Cargo.toml. Paths are relative to this file.
10+
# Re-comment before committing so CI/published wheels resolve from crates.io.
11+
# [patch.crates-io]
12+
# openjd-expr = { path = "../openjd-rs/crates/openjd-expr" }
13+
# openjd-model = { path = "../openjd-rs/crates/openjd-model" }
14+
# openjd-sessions = { path = "../openjd-rs/crates/openjd-sessions" }

THIRD-PARTY-LICENSES.txt

Lines changed: 48 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11

22

3-
** annotated-types; version 0.7.0 -- https://pypi.org/project/annotated-types/
3+
** annotated-types; version 0.8.0 -- https://pypi.org/project/annotated-types/
44
The MIT License (MIT)
55

66
Copyright (c) 2022 the contributors
@@ -611,7 +611,7 @@ PERFORMANCE OF THIS SOFTWARE.
611611
** encoding_rs; version 0.8.35 -- https://crates.io/crates/encoding_rs
612612
** nohash-hasher; version 0.2.0 -- https://crates.io/crates/nohash-hasher
613613
** static_assertions; version 1.1.0 -- https://crates.io/crates/static_assertions
614-
** tinyvec; version 1.11.0 -- https://crates.io/crates/tinyvec
614+
** tinyvec; version 1.12.0 -- https://crates.io/crates/tinyvec
615615

616616
Apache License
617617
Version 2.0, January 2004
@@ -1447,7 +1447,7 @@ PERFORMANCE OF THIS SOFTWARE.
14471447

14481448

14491449
------
1450-
** zerocopy; version 0.8.52 -- https://crates.io/crates/zerocopy
1450+
** zerocopy; version 0.8.55 -- https://crates.io/crates/zerocopy
14511451
Apache License
14521452
Version 2.0, January 2004
14531453
http://www.apache.org/licenses/
@@ -1860,11 +1860,11 @@ PERFORMANCE OF THIS SOFTWARE.
18601860

18611861

18621862
------
1863-
** futures-core; version 0.3.32 -- https://crates.io/crates/futures-core
1864-
** futures-macro; version 0.3.32 -- https://crates.io/crates/futures-macro
1865-
** futures-sink; version 0.3.32 -- https://crates.io/crates/futures-sink
1866-
** futures-task; version 0.3.32 -- https://crates.io/crates/futures-task
1867-
** futures-util; version 0.3.32 -- https://crates.io/crates/futures-util
1863+
** futures-core; version 0.3.33 -- https://crates.io/crates/futures-core
1864+
** futures-macro; version 0.3.33 -- https://crates.io/crates/futures-macro
1865+
** futures-sink; version 0.3.33 -- https://crates.io/crates/futures-sink
1866+
** futures-task; version 0.3.33 -- https://crates.io/crates/futures-task
1867+
** futures-util; version 0.3.33 -- https://crates.io/crates/futures-util
18681868
Apache License
18691869
Version 2.0, January 2004
18701870
http://www.apache.org/licenses/
@@ -2071,10 +2071,10 @@ limitations under the License.
20712071

20722072
------
20732073
** ahash; version 0.8.12 -- https://crates.io/crates/ahash
2074-
** arc-swap; version 1.9.1 -- https://crates.io/crates/arc-swap
2074+
** arc-swap; version 1.9.2 -- https://crates.io/crates/arc-swap
20752075
** base64; version 0.22.1 -- https://crates.io/crates/base64
2076-
** bitflags; version 2.13.0 -- https://crates.io/crates/bitflags
2077-
** bstr; version 1.12.1 -- https://crates.io/crates/bstr
2076+
** bitflags; version 2.13.1 -- https://crates.io/crates/bitflags
2077+
** bstr; version 1.13.0 -- https://crates.io/crates/bstr
20782078
** bumpalo; version 3.20.3 -- https://crates.io/crates/bumpalo
20792079
** cfg-if; version 1.0.4 -- https://crates.io/crates/cfg-if
20802080
** either; version 1.16.0 -- https://crates.io/crates/either
@@ -2086,28 +2086,28 @@ limitations under the License.
20862086
** heck; version 0.5.0 -- https://crates.io/crates/heck
20872087
** indexmap; version 2.14.0 -- https://crates.io/crates/indexmap
20882088
** itertools; version 0.14.0 -- https://crates.io/crates/itertools
2089-
** js-sys; version 0.3.102 -- https://crates.io/crates/js-sys
2090-
** log; version 0.4.32 -- https://crates.io/crates/log
2089+
** js-sys; version 0.3.103 -- https://crates.io/crates/js-sys
2090+
** log; version 0.4.33 -- https://crates.io/crates/log
20912091
** num-traits; version 0.2.19 -- https://crates.io/crates/num-traits
20922092
** once_cell; version 1.21.4 -- https://crates.io/crates/once_cell
20932093
** ordermap; version 1.2.0 -- https://crates.io/crates/ordermap
20942094
** pyo3-log; version 0.13.4 -- https://crates.io/crates/pyo3-log
2095-
** regex-automata; version 0.4.14 -- https://crates.io/crates/regex-automata
2095+
** regex-automata; version 0.4.16 -- https://crates.io/crates/regex-automata
20962096
** regex-syntax; version 0.8.11 -- https://crates.io/crates/regex-syntax
2097-
** regex; version 1.12.4 -- https://crates.io/crates/regex
2097+
** regex; version 1.13.1 -- https://crates.io/crates/regex
20982098
** signal-hook-registry; version 1.4.8 -- https://crates.io/crates/signal-hook-registry
20992099
** smallvec; version 1.15.2 -- https://crates.io/crates/smallvec
21002100
** unicode-normalization; version 0.1.25 -- https://crates.io/crates/unicode-normalization
21012101
** unicode-width; version 0.2.2 -- https://crates.io/crates/unicode-width
21022102
** unicode_names2; version 1.3.0 -- https://crates.io/crates/unicode_names2
2103-
** uuid; version 1.23.3 -- https://crates.io/crates/uuid
2104-
** value-bag; version 1.12.0 -- https://crates.io/crates/value-bag
2103+
** uuid; version 1.24.0 -- https://crates.io/crates/uuid
2104+
** value-bag; version 1.13.1 -- https://crates.io/crates/value-bag
21052105
** wasi; version 0.11.1+wasi-snapshot-preview1 -- https://crates.io/crates/wasi
21062106
** wasip2; version 1.0.4+wasi-0.2.12 -- https://crates.io/crates/wasip2
2107-
** wasm-bindgen-macro-support; version 0.2.125 -- https://crates.io/crates/wasm-bindgen-macro-support
2108-
** wasm-bindgen-macro; version 0.2.125 -- https://crates.io/crates/wasm-bindgen-macro
2109-
** wasm-bindgen-shared; version 0.2.125 -- https://crates.io/crates/wasm-bindgen-shared
2110-
** wasm-bindgen; version 0.2.125 -- https://crates.io/crates/wasm-bindgen
2107+
** wasm-bindgen-macro-support; version 0.2.126 -- https://crates.io/crates/wasm-bindgen-macro-support
2108+
** wasm-bindgen-macro; version 0.2.126 -- https://crates.io/crates/wasm-bindgen-macro
2109+
** wasm-bindgen-shared; version 0.2.126 -- https://crates.io/crates/wasm-bindgen-shared
2110+
** wasm-bindgen; version 0.2.126 -- https://crates.io/crates/wasm-bindgen
21112111
** wit-bindgen; version 0.57.1 -- https://crates.io/crates/wit-bindgen
21122112
Apache License
21132113
Version 2.0, January 2004
@@ -2522,36 +2522,37 @@ limitations under the License.
25222522
** arraydeque; version 0.5.1 -- https://crates.io/crates/arraydeque
25232523
** get-size-derive2; version 0.7.4 -- https://crates.io/crates/get-size-derive2
25242524
** get-size2; version 0.7.4 -- https://crates.io/crates/get-size2
2525-
** granit-parser; version 0.0.3 -- https://crates.io/crates/granit-parser
2525+
** granit-parser; version 0.0.7 -- https://crates.io/crates/granit-parser
25262526
** itoa; version 1.0.18 -- https://crates.io/crates/itoa
2527-
** libc; version 0.2.186 -- https://crates.io/crates/libc
2527+
** libc; version 0.2.189 -- https://crates.io/crates/libc
25282528
** manyhow-macros; version 0.11.4 -- https://crates.io/crates/manyhow-macros
2529-
** openjd-expr; version 0.1.2 -- https://crates.io/crates/openjd-expr
2530-
** openjd-model; version 0.3.0 -- https://crates.io/crates/openjd-model
2531-
** openjd-sessions; version 0.3.1 -- https://crates.io/crates/openjd-sessions
2529+
** openjd-expr; version 0.2.1 -- https://crates.io/crates/openjd-expr
2530+
** openjd-model; version 0.4.0 -- https://crates.io/crates/openjd-model
2531+
** openjd-sessions; version 0.4.0 -- https://crates.io/crates/openjd-sessions
25322532
** pin-project-lite; version 0.2.17 -- https://crates.io/crates/pin-project-lite
2533-
** portable-atomic; version 1.13.1 -- https://crates.io/crates/portable-atomic
2534-
** proc-macro2; version 1.0.106 -- https://crates.io/crates/proc-macro2
2533+
** portable-atomic; version 1.14.0 -- https://crates.io/crates/portable-atomic
2534+
** proc-macro2; version 1.0.107 -- https://crates.io/crates/proc-macro2
25352535
** pyo3-ffi; version 0.29.0 -- https://crates.io/crates/pyo3-ffi
25362536
** pyo3-macros-backend; version 0.29.0 -- https://crates.io/crates/pyo3-macros-backend
25372537
** pyo3-macros; version 0.29.0 -- https://crates.io/crates/pyo3-macros
25382538
** pyo3; version 0.29.0 -- https://crates.io/crates/pyo3
2539-
** quote; version 1.0.45 -- https://crates.io/crates/quote
2539+
** quote; version 1.0.47 -- https://crates.io/crates/quote
25402540
** r-efi; version 5.3.0 -- https://crates.io/crates/r-efi
25412541
** r-efi; version 6.0.0 -- https://crates.io/crates/r-efi
2542-
** rustc-hash; version 2.1.2 -- https://crates.io/crates/rustc-hash
2543-
** rustversion; version 1.0.22 -- https://crates.io/crates/rustversion
2542+
** rustc-hash; version 2.1.3 -- https://crates.io/crates/rustc-hash
2543+
** rustversion; version 1.0.23 -- https://crates.io/crates/rustversion
25442544
** ryu; version 1.0.23 -- https://crates.io/crates/ryu
2545-
** serde-saphyr; version 0.0.27 -- https://crates.io/crates/serde-saphyr
2546-
** serde; version 1.0.228 -- https://crates.io/crates/serde
2547-
** serde_core; version 1.0.228 -- https://crates.io/crates/serde_core
2548-
** serde_derive; version 1.0.228 -- https://crates.io/crates/serde_derive
2549-
** serde_json; version 1.0.150 -- https://crates.io/crates/serde_json
2545+
** serde-saphyr; version 0.0.29 -- https://crates.io/crates/serde-saphyr
2546+
** serde; version 1.0.229 -- https://crates.io/crates/serde
2547+
** serde_core; version 1.0.229 -- https://crates.io/crates/serde_core
2548+
** serde_derive; version 1.0.229 -- https://crates.io/crates/serde_derive
2549+
** serde_json; version 1.0.151 -- https://crates.io/crates/serde_json
25502550
** shlex; version 2.0.1 -- https://crates.io/crates/shlex
25512551
** siphasher; version 1.0.3 -- https://crates.io/crates/siphasher
2552-
** syn; version 2.0.118 -- https://crates.io/crates/syn
2553-
** thiserror-impl; version 2.0.18 -- https://crates.io/crates/thiserror-impl
2554-
** thiserror; version 2.0.18 -- https://crates.io/crates/thiserror
2552+
** syn; version 2.0.119 -- https://crates.io/crates/syn
2553+
** syn; version 3.0.3 -- https://crates.io/crates/syn
2554+
** thiserror-impl; version 2.0.19 -- https://crates.io/crates/thiserror-impl
2555+
** thiserror; version 2.0.19 -- https://crates.io/crates/thiserror
25552556
** unicode-ident; version 1.0.24 -- https://crates.io/crates/unicode-ident
25562557
Apache License
25572558
Version 2.0, January 2004
@@ -2659,7 +2660,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26592660

26602661

26612662
------
2662-
** mio; version 1.2.1 -- https://crates.io/crates/mio
2663+
** mio; version 1.2.2 -- https://crates.io/crates/mio
26632664
Copyright (c) 2014 Carl Lerche and other MIO contributors
26642665

26652666
Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -2682,7 +2683,7 @@ THE SOFTWARE.
26822683

26832684

26842685
------
2685-
** which; version 8.0.4 -- https://crates.io/crates/which
2686+
** which; version 8.0.5 -- https://crates.io/crates/which
26862687
Copyright (c) 2015 fangyuanziti
26872688

26882689
Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -2705,7 +2706,7 @@ THE SOFTWARE.
27052706

27062707

27072708
------
2708-
** bytes; version 1.11.1 -- https://crates.io/crates/bytes
2709+
** bytes; version 1.12.1 -- https://crates.io/crates/bytes
27092710
Copyright (c) 2018 Carl Lerche
27102711

27112712
Permission is hereby granted, free of charge, to any
@@ -2792,7 +2793,7 @@ DEALINGS IN THE SOFTWARE.
27922793

27932794

27942795
------
2795-
** tokio-macros; version 2.7.0 -- https://crates.io/crates/tokio-macros
2796+
** tokio-macros; version 2.7.1 -- https://crates.io/crates/tokio-macros
27962797
MIT License
27972798

27982799
Copyright (c) 2019 Yoshua Wuyts
@@ -2921,8 +2922,8 @@ USE OR OTHER DEALINGS IN THE SOFTWARE.
29212922

29222923

29232924
------
2924-
** tokio-util; version 0.7.18 -- https://crates.io/crates/tokio-util
2925-
** tokio; version 1.52.3 -- https://crates.io/crates/tokio
2925+
** tokio-util; version 0.7.19 -- https://crates.io/crates/tokio-util
2926+
** tokio; version 1.53.1 -- https://crates.io/crates/tokio
29262927
MIT License
29272928

29282929
Copyright (c) Tokio Contributors
@@ -2947,7 +2948,7 @@ SOFTWARE.
29472948

29482949

29492950
------
2950-
** zmij; version 1.0.21 -- https://crates.io/crates/zmij
2951+
** zmij; version 1.0.23 -- https://crates.io/crates/zmij
29512952
Permission is hereby granted, free of charge, to any
29522953
person obtaining a copy of this software and associated
29532954
documentation files (the "Software"), to deal in the
@@ -3000,7 +3001,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
30003001

30013002
------
30023003
** aho-corasick; version 1.1.4 -- https://crates.io/crates/aho-corasick
3003-
** memchr; version 2.8.2 -- https://crates.io/crates/memchr
3004+
** memchr; version 2.8.3 -- https://crates.io/crates/memchr
30043005
The MIT License (MIT)
30053006

30063007
Copyright (c) 2015 Andrew Gallant

rust-bindings/Cargo.toml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ name = "_openjd_rs"
1212
crate-type = ["cdylib", "rlib"]
1313

1414
[dependencies]
15-
openjd-expr = "0.1.2"
16-
openjd-model = "0.3.0"
17-
openjd-sessions = "0.3.1"
15+
openjd-expr = "0.2.1"
16+
openjd-model = "0.4.0"
17+
openjd-sessions = "0.4.0"
1818
tokio = { version = "1", features = ["rt-multi-thread"] }
1919
uuid = { version = "1", features = ["v4"] }
2020
serde_json = "1"
@@ -52,6 +52,10 @@ windows = { version = "0.62", features = [
5252
# wheel is built against the crates.io versions, which is what the
5353
# Cargo.lock file pins.
5454
#
55+
# NOTE: the [patch.crates-io] block that redirects these to a sibling
56+
# ~/openjd-rs checkout must live at the *workspace root* Cargo.toml — cargo
57+
# ignores `[patch]` in non-root package manifests. See ../Cargo.toml.
58+
#
5559
# [patch.crates-io]
5660
# openjd-expr = { path = "../../openjd-rs/crates/openjd-expr" }
5761
# openjd-model = { path = "../../openjd-rs/crates/openjd-model" }

rust-bindings/src/model/job.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -557,7 +557,7 @@ impl PyEmbeddedFile {
557557
fn new(
558558
name: String,
559559
r#type: String,
560-
filename: Option<crate::expr::PyFormatString>,
560+
filename: Option<String>,
561561
data: Option<crate::expr::PyFormatString>,
562562
runnable: Option<bool>,
563563
#[allow(non_snake_case)] endOfLine: Option<String>,
@@ -586,7 +586,9 @@ impl PyEmbeddedFile {
586586
inner: job::EmbeddedFile {
587587
name,
588588
file_type,
589-
filename: filename.map(|f| f.inner),
589+
// Plain string per the 2023-09 schema: the field is not
590+
// @fmtstring, so "{{ }}" sequences are literal text.
591+
filename,
590592
data: data.map(|d| d.inner),
591593
runnable,
592594
end_of_line,
@@ -607,7 +609,7 @@ impl PyEmbeddedFile {
607609

608610
#[getter]
609611
fn filename(&self) -> Option<String> {
610-
self.inner.filename.as_ref().map(|f| f.raw().to_string())
612+
self.inner.filename.clone()
611613
}
612614

613615
#[getter]
@@ -1085,6 +1087,9 @@ impl PyCancelationMode {
10851087
match &self.inner {
10861088
job::CancelationMode::Terminate => "TERMINATE",
10871089
job::CancelationMode::NotifyThenTerminate { .. } => "NOTIFY_THEN_TERMINATE",
1090+
// The raw format string; the mode decision is deferred to run
1091+
// time. Matches the openjd-rs serialization of DeferredMode.
1092+
job::CancelationMode::DeferredMode { mode, .. } => mode.raw(),
10881093
}
10891094
}
10901095

@@ -1093,6 +1098,10 @@ impl PyCancelationMode {
10931098
match &self.inner {
10941099
job::CancelationMode::NotifyThenTerminate {
10951100
notify_period_in_seconds,
1101+
}
1102+
| job::CancelationMode::DeferredMode {
1103+
notify_period_in_seconds,
1104+
..
10961105
} => notify_period_in_seconds
10971106
.as_ref()
10981107
.map(|t| t.raw().to_string()),

rust-bindings/src/model/template_types.rs

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,18 @@ impl PyCancelationMode {
159159
"NOTIFY_THEN_TERMINATE" => CancelationMode::NotifyThenTerminate {
160160
notify_period_in_seconds: notify_period_in_seconds.map(|fs| fs.inner),
161161
},
162+
// A format-string mode (FEATURE_BUNDLE_1) defers the
163+
// TERMINATE-vs-NOTIFY_THEN_TERMINATE decision to run time.
164+
// Mirrors the openjd-rs serde impl, which routes any mode
165+
// containing "{{" to CancelationMode::DeferredMode.
166+
other if other.contains("{{") => CancelationMode::DeferredMode {
167+
mode: openjd_expr::FormatString::new(other).map_err(|e| {
168+
pyo3::exceptions::PyValueError::new_err(format!(
169+
"invalid cancelation mode format string: {e}"
170+
))
171+
})?,
172+
notify_period_in_seconds: notify_period_in_seconds.map(|fs| fs.inner),
173+
},
162174
other => {
163175
return Err(pyo3::exceptions::PyValueError::new_err(format!(
164176
"unknown cancelation mode: {other}"
@@ -173,6 +185,9 @@ impl PyCancelationMode {
173185
match &self.inner {
174186
CancelationMode::Terminate => "TERMINATE",
175187
CancelationMode::NotifyThenTerminate { .. } => "NOTIFY_THEN_TERMINATE",
188+
// The raw format string; the mode decision is deferred to run
189+
// time. Matches the openjd-rs serialization of DeferredMode.
190+
CancelationMode::DeferredMode { mode, .. } => mode.raw(),
176191
}
177192
}
178193

@@ -182,6 +197,10 @@ impl PyCancelationMode {
182197
CancelationMode::Terminate => None,
183198
CancelationMode::NotifyThenTerminate {
184199
notify_period_in_seconds,
200+
}
201+
| CancelationMode::DeferredMode {
202+
notify_period_in_seconds,
203+
..
185204
} => notify_period_in_seconds
186205
.as_ref()
187206
.map(|fs| PyFormatString { inner: fs.clone() }),
@@ -208,6 +227,19 @@ impl PyCancelationMode {
208227
"CancelationMode(mode='NOTIFY_THEN_TERMINATE', notify_period_in_seconds={raw})"
209228
)
210229
}
230+
CancelationMode::DeferredMode {
231+
mode,
232+
notify_period_in_seconds,
233+
} => {
234+
let raw = notify_period_in_seconds
235+
.as_ref()
236+
.map(|fs| format!("{:?}", fs.raw()))
237+
.unwrap_or_else(|| "None".to_string());
238+
format!(
239+
"CancelationMode(mode={:?}, notify_period_in_seconds={raw})",
240+
mode.raw()
241+
)
242+
}
211243
}
212244
}
213245

@@ -524,7 +556,7 @@ impl PyEmbeddedFile {
524556
fn new(
525557
name: String,
526558
r#type: &str,
527-
filename: Option<PyFormatString>,
559+
filename: Option<String>,
528560
data: Option<PyFormatString>,
529561
runnable: Option<bool>,
530562
end_of_line: Option<&str>,
@@ -533,7 +565,9 @@ impl PyEmbeddedFile {
533565
inner: EmbeddedFile {
534566
name,
535567
file_type: parse_file_type(r#type)?,
536-
filename: filename.map(|fs| fs.inner),
568+
// Plain string per the 2023-09 schema: the field is not
569+
// @fmtstring, so "{{ }}" sequences are literal text.
570+
filename,
537571
data: data.map(|fs| fs.inner),
538572
runnable,
539573
end_of_line: end_of_line.map(parse_eol).transpose()?,
@@ -553,11 +587,8 @@ impl PyEmbeddedFile {
553587
}
554588

555589
#[getter]
556-
fn filename(&self) -> Option<PyFormatString> {
557-
self.inner
558-
.filename
559-
.as_ref()
560-
.map(|fs| PyFormatString { inner: fs.clone() })
590+
fn filename(&self) -> Option<String> {
591+
self.inner.filename.clone()
561592
}
562593

563594
#[getter]

0 commit comments

Comments
 (0)