Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions THIRD-PARTY-LICENSES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2534,9 +2534,9 @@ limitations under the License.
** itoa; version 1.0.18 -- https://crates.io/crates/itoa
** libc; version 0.2.189 -- https://crates.io/crates/libc
** manyhow-macros; version 0.11.4 -- https://crates.io/crates/manyhow-macros
** openjd-expr; version 0.5.0 -- https://crates.io/crates/openjd-expr
** openjd-model; version 0.5.4 -- https://crates.io/crates/openjd-model
** openjd-sessions; version 0.5.4 -- https://crates.io/crates/openjd-sessions
** openjd-expr; version 0.6.0 -- https://crates.io/crates/openjd-expr
** openjd-model; version 0.6.0 -- https://crates.io/crates/openjd-model
** openjd-sessions; version 0.5.5 -- https://crates.io/crates/openjd-sessions
** pin-project-lite; version 0.2.17 -- https://crates.io/crates/pin-project-lite
** portable-atomic; version 1.15.0 -- https://crates.io/crates/portable-atomic
** proc-macro2; version 1.0.107 -- https://crates.io/crates/proc-macro2
Expand Down
6 changes: 3 additions & 3 deletions rust-bindings/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ name = "_openjd_rs"
crate-type = ["cdylib", "rlib"]

[dependencies]
openjd-expr = "0.5.0"
openjd-model = "0.5.4"
openjd-sessions = "0.5.4"
openjd-expr = "0.6.0"
openjd-model = "0.6.0"
openjd-sessions = "0.5.5"
Comment thread
leongdl marked this conversation as resolved.
tokio = { version = "1", features = ["rt-multi-thread"] }
uuid = { version = "1", features = ["v4"] }
serde_json = "1"
Expand Down
16 changes: 13 additions & 3 deletions rust-bindings/src/model/job.rs
Original file line number Diff line number Diff line change
Expand Up @@ -899,15 +899,25 @@ fn task_param_def_from_dict(
// List of values — coerce each to the variant's element type.
let mut items: Vec<serde_json::Value> = Vec::with_capacity(list.len());
for v in list.iter() {
let s: String = match v.extract::<String>() {
Ok(s) => s,
Err(_) => v.str()?.extract()?,
// Whether the element arrived as a Python `str` decides which member of
// the template's own `<float> | <floatstring>` union it is, and a
// `<floatstring>` has to keep the spelling it was written with (§7.5).
// `Float64` reads a JSON string as that spelling and a JSON number as a
// bare value, so the distinction has to survive to here -- `v.str()`
// collapses it.
let (s, is_text): (String, bool) = match v.extract::<String>() {
Ok(s) => (s, true),
Err(_) => (v.str()?.extract()?, false),
};
items.push(match variant {
"int" | "chunkInt" => match s.parse::<i64>() {
Ok(n) => serde_json::Value::Number(n.into()),
Err(_) => serde_json::Value::String(s),
},
// The text carries as-is. `Float64`'s deserializer parses it, and
// rejects an unparseable one there rather than here -- which is also
// what happened when this re-emitted a number.
"float" if is_text => serde_json::Value::String(s),
"float" => match s.parse::<f64>() {
Ok(n) => serde_json::Number::from_f64(n)
.map(serde_json::Value::Number)
Expand Down
27 changes: 20 additions & 7 deletions rust-bindings/src/model/step_param_space.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,26 @@ impl PyStepParameterSpaceIterator {
}

fn __getitem__(&self, py: Python<'_>, index: isize) -> PyResult<Py<PyDict>> {
// Random access uses a fresh iterator — don't disturb the
// persistent iter's cursor or its adaptive Arc. It must carry the
// same chunk override, or indexing would report chunks that
// iteration never yields.
let iter =
StepParameterSpaceIterator::new_with_chunk_override(&self.space, self.chunk_override)
.map_err(model_err_to_py)?;
// Rejected here rather than left to `get`, because the negative-index
// arithmetic below resolves against `self.len` — the count `__len__`
// refuses to report for an adaptive space. Without this, `it[-1]` declines
// only incidentally, and would start answering against that hidden count
// if `get` were ever extended to adaptive spaces the way 0.6.0 extended it
// to contiguous ones. The message is the pure-Python reference's; the type
// stays `IndexError`, which is a `LookupError` as v0 raises, so `except`
// clauses for either keep working.
if iter.chunks_adaptive() {
return Err(pyo3::exceptions::PyIndexError::new_err(
"Items cannot be retrieved by index because the parameter space uses adaptive chunking.",
));
}
let idx = if index < 0 {
let adjusted = self.len as isize + index;
if adjusted < 0 {
Expand All @@ -225,13 +245,6 @@ impl PyStepParameterSpaceIterator {
} else {
index as usize
};
// Random access uses a fresh iterator — don't disturb the
// persistent iter's cursor or its adaptive Arc. It must carry the
// same chunk override, or indexing would report chunks that
// iteration never yields.
let iter =
StepParameterSpaceIterator::new_with_chunk_override(&self.space, self.chunk_override)
.map_err(model_err_to_py)?;
match iter.get(idx) {
Some(params) => task_param_set_to_py(py, &params),
None => Err(pyo3::exceptions::PyIndexError::new_err(
Expand Down
6 changes: 5 additions & 1 deletion rust-bindings/src/model/task_parameter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -548,8 +548,12 @@ pub(crate) fn task_parameter_to_py<'py>(
}
.into_bound_py_any(py)
}
// `Float64` carries the spelling a `<floatstring>` range element was
// written with (§7.5). It reaches a command line through
// `TaskParameterValue`, which renders it verbatim; this getter is the
// numeric introspection view, so take the value and drop the spelling.
TaskParameter::Float { range } => PyFloatTaskParameter {
range: range.clone(),
range: range.iter().map(|f| f.value()).collect(),
Comment thread
leongdl marked this conversation as resolved.
}
.into_bound_py_any(py),
TaskParameter::String { range } => PyStringTaskParameter {
Expand Down
7 changes: 3 additions & 4 deletions scripts/check_third_party_licenses.sh
Original file line number Diff line number Diff line change
Expand Up @@ -150,16 +150,15 @@ awk -v re="^[*][*] ($workspace_pattern); version " '

# ── Combine ───────────────────────────────────────────────────────────

# Piped through `sed` on the way in, so the EOL strip needs neither a second temp
# file nor `sed -i`, which wants a backup suffix on BSD sed and refuses one on GNU.
{
echo ""
echo ""
cat "$python_section"
echo ""
cat "$rust_section"
} > "$generated"

# Ensure consistent EOL.
sed -i 's/\r//' "$generated"
} | sed 's/\r//' > "$generated"

if [[ "$mode" == "update" ]]; then
cp "$generated" "$OUTPUT_FILE"
Expand Down
56 changes: 42 additions & 14 deletions specs/python-model-interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -718,6 +718,24 @@ does. (The underlying Rust struct has `chunks: Option<ResolvedChunks>`
on the `Int` variant for shape reasons, but no resolver path ever
populates it; the binding mirrors the runtime *behaviour*.)

`FloatTaskParameter.range` is numeric, so it does not show the decimal
places a `<floatstring>` range element was written with. A range element
`'2.50'` reports `2.5` here and renders `2.50` as the task parameter
value, which is the form that reaches a command line (Template Schemas
§7.5). Two `FloatTaskParameter`s that compare equal by `range` can
therefore render different task values. Read
`StepParameterSpaceIterator` for the rendered form.

Constructing a space directly follows the same rule as the template: a
range element given as a `str` is a `<floatstring>` and keeps its
spelling, and one given as a `float` is a `<float>` and renders as the
number. `StepParameterSpace(taskParameterDefinitions={"F": {"type":
"FLOAT", "range": ["1.50"]}})` renders `1.50`, and `range=[1.5]` renders
`1.5`. Stripping a redundant leading zero is a `create_job`
normalization rather than part of reading a resolved value, so `'02.50'`
given directly to the constructor keeps its zero where the same element
in a template does not.

### `ChunkIntTaskParameter`

Available only when the `TASK_CHUNKING` extension is enabled.
Expand Down Expand Up @@ -1176,20 +1194,30 @@ least 1, so 0 would otherwise silently mean 1, and the
`chunks_default_task_count` setter already rejects it. The pure-Python
reference does not validate this argument.

Two current-implementation limitations, both divergences from the v0
reference rather than intended behaviour. Each has a `strict` xfail in
`test/openjd/model_v1/test_known_gaps.py`, so clearing either will fail
CI until this text is updated with it.

- Indexing observes the override only for a space that supports random
access. A `CONTIGUOUS` chunked space requires sequential iteration, so
`it[i]` raises `IndexError` for any index — with or without the
override — even though `len(it)` reports a count. See
`test_a_contiguous_chunked_space_supports_indexing`.
- `chunks_parameter_name` and `chunks_default_task_count` both return
`None` once the space is non-adaptive, which supplying the override
makes it. v0 reports the parameter name and the override value. See
`test_chunk_metadata_is_reported_for_a_non_adaptive_space`.
Indexing observes the override. `openjd-model` 0.6.0 gave a `CONTIGUOUS`
chunked space random access, so `it[i]` answers there as it does for a
`NONCONTIGUOUS` one, and reports the overridden granularity rather than
the template's.

Comment thread
leongdl marked this conversation as resolved.
One current-implementation limitation remains. An *adaptive* space has no
knowable count until it is walked, so `len(it)` raises `ValueError` and
`it[i]` is refused for every index, negative included. Iteration still
yields, which is what distinguishes an unknown count from an empty space.
Supplying the override makes the space static and lifts both.

That refusal is enforced rather than incidental: `__getitem__` rejects an
adaptive space before it resolves a negative index, because the
arithmetic for a negative index would otherwise run against the length
`__len__` declines to report.

One divergence from the v0 reference here, in the exception *type*. v0
raises a bare `LookupError` for an adaptive `it[i]`; v1 raises
`IndexError`, which is a `LookupError` subclass, so a caller catching
either type is served by both implementations. The messages agree.
Because `IndexError` also means "past the end" in v1, the two conditions
are told apart by the message rather than the type — the adaptive refusal
says `Items cannot be retrieved by index because the parameter space uses
adaptive chunking.` where a real overrun says `index out of range`.

### `StepDependencyGraph`

Expand Down
120 changes: 0 additions & 120 deletions test/openjd/model_v1/test_known_gaps.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@

from __future__ import annotations

from typing import Any

# ── Top-level package no longer leaks typing imports ──
#
# An earlier draft of ``openjd.model._v1`` imported ``Any``,
Expand All @@ -43,121 +41,3 @@ def test_no_internal_imports_leak_at_top_level(name: str) -> None:
import openjd.model._v1 as v1

assert not hasattr(v1, name), f"{name} leaks as a public attribute on openjd.model._v1"


# ── Chunked parameter spaces: two divergences from the v0 reference ──
#
# Found while adding `chunks_task_count_override` to
# `StepParameterSpaceIterator`. Neither is caused by that argument — both
# reproduce without it — so they are recorded here rather than fixed in
# passing.


def _chunked_step(constraint: str) -> Any:
from openjd.model._v1 import create_job, decode_job_template

template = {
"specificationVersion": "jobtemplate-2023-09",
"name": "T",
"extensions": ["TASK_CHUNKING"],
"steps": [
{
"name": "S",
"parameterSpace": {
"taskParameterDefinitions": [
{
"name": "Frame",
"type": "CHUNK[INT]",
"range": "1-10",
"chunks": {"defaultTaskCount": 5, "rangeConstraint": constraint},
}
]
},
"script": {
"actions": {"onRun": {"command": "echo", "args": ["{{Task.Param.Frame}}"]}}
},
}
],
}
job_template = decode_job_template(template=template, supported_extensions=["TASK_CHUNKING"])
return create_job(job_template=job_template, job_parameter_values={}).steps[0]


@pytest.mark.xfail(
reason="v1 derives chunks_parameter_name and chunks_default_task_count from adaptive "
"detection, so both are None for any non-adaptive chunked space. v0 reports them for "
"any chunked space. See openjd-model step_param_space.rs: chunks_param_name and "
"adaptive_chunk_size are both built from adaptive_info.",
strict=True,
)
@pytest.mark.parametrize(
"chunks,override,expected_count",
[
# Statically chunked: no override involved, v0 reports the template's size.
({"defaultTaskCount": 5, "rangeConstraint": "CONTIGUOUS"}, None, 5),
# Statically chunked, re-chunked by the override.
({"defaultTaskCount": 5, "rangeConstraint": "CONTIGUOUS"}, 1, 1),
# Adaptive, turned static by the override. v0 reports the override as the size.
(
{"defaultTaskCount": 5, "targetRuntimeSeconds": 60, "rangeConstraint": "CONTIGUOUS"},
1,
1,
),
],
ids=["static", "static-overridden", "adaptive-overridden"],
)
def test_chunk_metadata_is_reported_for_a_non_adaptive_space(
chunks: dict, override: int | None, expected_count: int
) -> None:
"""v0 returns ``"Frame"`` and the chunk size for each of these. v1 returns ``None``.

Neither value is unknowable — both are in the template, or are the override the caller
just passed — so a consumer inspecting a non-adaptive chunked space through v1 cannot
learn which parameter chunks, or at what size. One root cause, three ways to reach it:
anything that leaves the space non-adaptive drops both getters.
"""
from openjd.model._v1 import create_job, decode_job_template
from openjd.model._v1.job import StepParameterSpaceIterator

template = {
"specificationVersion": "jobtemplate-2023-09",
"name": "T",
"extensions": ["TASK_CHUNKING"],
"steps": [
{
"name": "S",
"parameterSpace": {
"taskParameterDefinitions": [
{"name": "Frame", "type": "CHUNK[INT]", "range": "1-10", "chunks": chunks}
]
},
"script": {
"actions": {"onRun": {"command": "echo", "args": ["{{Task.Param.Frame}}"]}}
},
}
],
}
job_template = decode_job_template(template=template, supported_extensions=["TASK_CHUNKING"])
step = create_job(job_template=job_template, job_parameter_values={}).steps[0]

it = StepParameterSpaceIterator(step=step, chunks_task_count_override=override)
assert it.chunks_adaptive is False
assert it.chunks_parameter_name == "Frame"
assert it.chunks_default_task_count == expected_count


@pytest.mark.xfail(
reason="v1 refuses random access whenever the space needs sequential iteration, and "
"contiguous chunking always does. v0 supports indexing the same space.",
strict=True,
)
def test_a_contiguous_chunked_space_supports_indexing() -> None:
"""v0 answers ``it[0]`` with ``1-5``. v1 raises ``IndexError``.

``len()`` works on this space, so the count is known; only ``get`` declines.
"""
from openjd.model._v1.job import StepParameterSpaceIterator

it = StepParameterSpaceIterator(step=_chunked_step("CONTIGUOUS"))
assert len(it) == 2
assert it[0]["Frame"].value == "1-5"
Loading
Loading