From a8e2ff76c0f7ee9741f395ca9552ba6a333ced23 Mon Sep 17 00:00:00 2001 From: Vyron Vasileiadis Date: Mon, 7 Sep 2026 18:43:50 +0300 Subject: [PATCH 1/4] Require a SweepSource in a sweep's source position Sweep now takes a source and nothing else. A sequence of points raises a ValidationError naming Values() and .from_values(), so the block always holds something that can answer its own length and kind and write itself back out to .qp. --- changelog/41.changed.md | 1 + docs/guide/control-flow.md | 6 +++--- docs/reference/errors.md | 2 +- src/qprogram/blocks/sweep.py | 37 +++++++++++++++++------------------- src/qprogram/qprogram.py | 9 ++++----- tests/test_blocks.py | 12 ++++++------ 6 files changed, 32 insertions(+), 35 deletions(-) create mode 100644 changelog/41.changed.md diff --git a/changelog/41.changed.md b/changelog/41.changed.md new file mode 100644 index 0000000..e1fc1b7 --- /dev/null +++ b/changelog/41.changed.md @@ -0,0 +1 @@ +`Sweep` takes a `SweepSource` and nothing else. A sequence of points in the source position is now a `ValidationError` naming the two spellings that pick explicit values, `sweep(variable, qp.Values([...]))` and `sweep(variable).from_values([...])`, so the block always holds something that can answer its own length and kind and write itself back out to `.qp`. The combinators are unaffected, and so is the format's bracket literal, which still reads back as `Values`. diff --git a/docs/guide/control-flow.md b/docs/guide/control-flow.md index 85319ec..7ad45e3 100644 --- a/docs/guide/control-flow.md +++ b/docs/guide/control-flow.md @@ -53,9 +53,9 @@ than the `rotate` and `repeat` shortcuts below reach. An omitted source is detected with a sentinel rather than `None`, so `sweep(freq, None)`, a source that failed to be computed, is rejected instead of -quietly returning a builder: `Sweep source must be a SweepSource or a 1-D -sequence of values, got None`. A bare 1-D sequence in the source position is -accepted as shorthand for `qp.Values`. +quietly returning a builder: `Sweep source must be a SweepSource, got None`. The +source position takes a source and nothing else, so an explicit list of points +is written as `qp.Values([...])` or picked with `.from_values([...])`. A builder is not a context manager, because it has no values yet. Entering one raises `ValidationError` listing the `from_*` methods and the two-argument form, diff --git a/docs/reference/errors.md b/docs/reference/errors.md index 5330647..af01339 100644 --- a/docs/reference/errors.md +++ b/docs/reference/errors.md @@ -71,7 +71,7 @@ built the offending node. | `qprogram.fragments` | A fragment name that is malformed or reserved, a parameter colliding with a local variable, the wrong number of call arguments, an unknown or duplicated keyword, an unsupported argument type, a call cycle, and an expansion result that is not a bus or a waveform | | `qprogram.sweeps.builtin` | Non-numeric or non-finite bounds, `num < 1`, a zero step, a step pointing away from `stop`, non-positive `Logspace` bounds, and a `Values` or `File` array that is empty or not 1-D | | `qprogram.sweeps.combinators` | A `Repeat` count below 1, a non-integer `Rotate` offset, `Concat` given a single source or none, and any combinator argument that is a callable rather than a `SweepSource` | -| `qprogram.blocks` | A `Sweep` source that is a callable or not a 1-D sequence, a `Parallel` with fewer than two loops or with mismatched iteration counts, `Average(shots)` below 1, and appending directly to a `Conditional` | +| `qprogram.blocks` | A `Sweep` source that is not a `SweepSource`, a `Parallel` with fewer than two loops or with mismatched iteration counts, `Average(shots)` below 1, and appending directly to a `Conditional` | | `qprogram.operations.operation` | A `fields=` that is a bare string, not iterable, empty, or names a field no capability token registers | | `qprogram.waveforms.iq_pair` | An `IQPair` whose I and Q channels have different concrete durations | | `qprogram.result` | An empty `MeasurementHandle` name, and `QProgramResult.get(field=None)` | diff --git a/src/qprogram/blocks/sweep.py b/src/qprogram/blocks/sweep.py index 4d5c940..8fc5fd5 100644 --- a/src/qprogram/blocks/sweep.py +++ b/src/qprogram/blocks/sweep.py @@ -34,11 +34,11 @@ class Sweep(Block): Args: variable (Variable): The [`Variable`][qprogram.Variable] rebound on each iteration. - source (SweepSource): The [`SweepSource`][qprogram.SweepSource] describing the values. A bare - 1-D sequence is accepted as a shorthand for [`Values`][qprogram.Values]. + source (SweepSource): The [`SweepSource`][qprogram.SweepSource] describing the values. An + explicit list of points is [`Values`][qprogram.Values]. Raises: - ValidationError: If ``source`` is neither a source nor a sequence of values. + ValidationError: If ``source`` is not a [`SweepSource`][qprogram.SweepSource]. """ REPEATS: ClassVar[bool] = True @@ -47,7 +47,7 @@ class Sweep(Block): def __init__(self, variable: Variable, source: SweepSource) -> None: super().__init__() self.variable = variable - self.source = _coerce_source(source) + self.source = _require_source(source) def num_iterations(self) -> int: """Return the number of sweep points, delegated to the source. @@ -87,25 +87,22 @@ def required_capabilities(self) -> set[str]: return {"block.sweep"} | self.source.tokens() -def _coerce_source(source: object) -> SweepSource: - """Return ``source`` as a [`SweepSource`][qprogram.SweepSource], wrapping a bare sequence. +def _require_source(source: object) -> SweepSource: + """Return ``source`` unchanged, refusing anything that is not a [`SweepSource`][qprogram.SweepSource]. - A sequence is the shorthand spelling of [`Values`][qprogram.Values]. A callable is refused - outright: a deferred function can report neither its length nor its kind before the program runs, - and cannot be serialized to ``.qp``. + The block holds a description of the values, never the values themselves: a source answers its + length and its kind before the program runs, and writes itself back out to ``.qp``. A callable + gets its own message, since it answers none of that at any point. Args: - source (object): A sweep source, or a 1-D sequence of values to wrap. + source (object): The value offered as the sweep's source. Returns: - The source unchanged, or a [`Values`][qprogram.Values] over the given points. + ``source``, once it is known to be a sweep source. Raises: - ValidationError: If ``source`` is a callable, or is not a 1-D sequence of values. + ValidationError: If ``source`` is not a [`SweepSource`][qprogram.SweepSource]. """ - # qprogram.sweeps imports this module, so the shorthand's import stays lazy. - from qprogram.sweeps.builtin import Values # ruff: ignore[import-outside-top-level] - if isinstance(source, SweepSource): return source if callable(source): @@ -116,8 +113,8 @@ def _coerce_source(source: object) -> SweepSource: "SweepSource subclass with the parameters it needs." ) raise ValidationError(msg) - try: - return Values(source) # ty:ignore[invalid-argument-type] - except (ValidationError, TypeError, ValueError) as e: - msg = f"Sweep source must be a SweepSource or a 1-D sequence of values, got {source!r}" - raise ValidationError(msg) from e + msg = ( + f"Sweep source must be a SweepSource, got {source!r}. An explicit list of points is " + f"Values(...) — sweep(variable, qp.Values([...])) or sweep(variable).from_values([...])." + ) + raise ValidationError(msg) diff --git a/src/qprogram/qprogram.py b/src/qprogram/qprogram.py index ff0687c..f88b416 100644 --- a/src/qprogram/qprogram.py +++ b/src/qprogram/qprogram.py @@ -1311,17 +1311,16 @@ def sweep(self, variable: Variable, source: SweepSource | _Unset = _UNSET) -> _S Args: variable (Variable): The [`Variable`][qprogram.Variable] rebound each iteration. - source (SweepSource, optional): A [`SweepSource`][qprogram.SweepSource]. A bare 1-D - sequence is accepted as shorthand for [`Values`][qprogram.Values]. Omit it to - get a `_SweepBuilder` and pick the values with a ``from_*`` method instead. + source (SweepSource, optional): A [`SweepSource`][qprogram.SweepSource]; an explicit + list of points is [`Values`][qprogram.Values]. Omit it to get a `_SweepBuilder` and + pick the values with a ``from_*`` method instead. Returns: A context manager opening the sweep block, or — when ``source`` is omitted — the `_SweepBuilder` that produces one. Raises: - ValidationError: If ``source`` is given but is neither a sweep source nor a 1-D sequence - of values. + ValidationError: If ``source`` is given but is not a sweep source. """ if isinstance(source, _Unset): return _SweepBuilder(self, variable) diff --git a/tests/test_blocks.py b/tests/test_blocks.py index 1f2a813..f2defd8 100644 --- a/tests/test_blocks.py +++ b/tests/test_blocks.py @@ -191,10 +191,10 @@ def test_sweep_repeats(): assert Sweep.REPEATS is True -def test_sweep_accepts_a_bare_sequence_as_values_shorthand(): - sw = Sweep(Variable("x"), [0.1, 0.2, 0.3]) - assert isinstance(sw.source, Values) - assert sw.num_iterations() == 3 +def test_sweep_rejects_a_sequence_of_points(): + """An explicit list of points is a `Values`, which the message says.""" + with pytest.raises(ValidationError, match="must be a SweepSource"): + Sweep(Variable("x"), [0.1, 0.2, 0.3]) # ty:ignore[invalid-argument-type] def test_sweep_rejects_a_callable_source(): @@ -204,9 +204,9 @@ def test_sweep_rejects_a_callable_source(): Sweep(v, lambda i: i) # ty:ignore[invalid-argument-type] -def test_sweep_rejects_a_non_source_non_sequence(): +def test_sweep_rejects_a_non_source(): v = Variable("x") - with pytest.raises(ValidationError, match="SweepSource or a 1-D sequence"): + with pytest.raises(ValidationError, match="must be a SweepSource"): Sweep(v, object()) # ty:ignore[invalid-argument-type] From 6965439d7f6f156eac6d216681bccaa103ef079c Mon Sep 17 00:00:00 2001 From: Vyron Vasileiadis Date: Mon, 7 Sep 2026 18:52:37 +0300 Subject: [PATCH 2/4] Cut the Values pointer from the sweep source docstrings and error --- src/qprogram/blocks/sweep.py | 8 ++------ src/qprogram/qprogram.py | 5 ++--- tests/test_blocks.py | 2 +- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/qprogram/blocks/sweep.py b/src/qprogram/blocks/sweep.py index 8fc5fd5..390c785 100644 --- a/src/qprogram/blocks/sweep.py +++ b/src/qprogram/blocks/sweep.py @@ -34,8 +34,7 @@ class Sweep(Block): Args: variable (Variable): The [`Variable`][qprogram.Variable] rebound on each iteration. - source (SweepSource): The [`SweepSource`][qprogram.SweepSource] describing the values. An - explicit list of points is [`Values`][qprogram.Values]. + source (SweepSource): The [`SweepSource`][qprogram.SweepSource] describing the values. Raises: ValidationError: If ``source`` is not a [`SweepSource`][qprogram.SweepSource]. @@ -113,8 +112,5 @@ def _require_source(source: object) -> SweepSource: "SweepSource subclass with the parameters it needs." ) raise ValidationError(msg) - msg = ( - f"Sweep source must be a SweepSource, got {source!r}. An explicit list of points is " - f"Values(...) — sweep(variable, qp.Values([...])) or sweep(variable).from_values([...])." - ) + msg = f"Sweep source must be a SweepSource, got {source!r}" raise ValidationError(msg) diff --git a/src/qprogram/qprogram.py b/src/qprogram/qprogram.py index f88b416..f97ae85 100644 --- a/src/qprogram/qprogram.py +++ b/src/qprogram/qprogram.py @@ -1311,9 +1311,8 @@ def sweep(self, variable: Variable, source: SweepSource | _Unset = _UNSET) -> _S Args: variable (Variable): The [`Variable`][qprogram.Variable] rebound each iteration. - source (SweepSource, optional): A [`SweepSource`][qprogram.SweepSource]; an explicit - list of points is [`Values`][qprogram.Values]. Omit it to get a `_SweepBuilder` and - pick the values with a ``from_*`` method instead. + source (SweepSource, optional): A [`SweepSource`][qprogram.SweepSource]. Omit it to + get a `_SweepBuilder` and pick the values with a ``from_*`` method instead. Returns: A context manager opening the sweep block, or — when ``source`` is omitted — the diff --git a/tests/test_blocks.py b/tests/test_blocks.py index f2defd8..b8ce9ba 100644 --- a/tests/test_blocks.py +++ b/tests/test_blocks.py @@ -192,7 +192,7 @@ def test_sweep_repeats(): def test_sweep_rejects_a_sequence_of_points(): - """An explicit list of points is a `Values`, which the message says.""" + """The block binds a source, not the points a source would produce.""" with pytest.raises(ValidationError, match="must be a SweepSource"): Sweep(Variable("x"), [0.1, 0.2, 0.3]) # ty:ignore[invalid-argument-type] From 9f376bb1fbc8cca3145c361b809869cc50c5f09a Mon Sep 17 00:00:00 2001 From: Vyron Vasileiadis Date: Mon, 7 Sep 2026 18:54:48 +0300 Subject: [PATCH 3/4] Hoist the variable out of the sweep source rejection test --- tests/test_blocks.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_blocks.py b/tests/test_blocks.py index b8ce9ba..622c697 100644 --- a/tests/test_blocks.py +++ b/tests/test_blocks.py @@ -193,8 +193,9 @@ def test_sweep_repeats(): def test_sweep_rejects_a_sequence_of_points(): """The block binds a source, not the points a source would produce.""" + v = Variable("x") with pytest.raises(ValidationError, match="must be a SweepSource"): - Sweep(Variable("x"), [0.1, 0.2, 0.3]) # ty:ignore[invalid-argument-type] + Sweep(v, [0.1, 0.2, 0.3]) # ty:ignore[invalid-argument-type] def test_sweep_rejects_a_callable_source(): From 8b2ddb0b1b3e8df5e4185b3f990d5cbc138916b5 Mon Sep 17 00:00:00 2001 From: Vyron Vasileiadis Date: Mon, 7 Sep 2026 18:59:02 +0300 Subject: [PATCH 4/4] Drop the Values pointer from the sweep source paragraph --- docs/guide/control-flow.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/guide/control-flow.md b/docs/guide/control-flow.md index 7ad45e3..8341f01 100644 --- a/docs/guide/control-flow.md +++ b/docs/guide/control-flow.md @@ -53,9 +53,7 @@ than the `rotate` and `repeat` shortcuts below reach. An omitted source is detected with a sentinel rather than `None`, so `sweep(freq, None)`, a source that failed to be computed, is rejected instead of -quietly returning a builder: `Sweep source must be a SweepSource, got None`. The -source position takes a source and nothing else, so an explicit list of points -is written as `qp.Values([...])` or picked with `.from_values([...])`. +quietly returning a builder: `Sweep source must be a SweepSource, got None`. A builder is not a context manager, because it has no values yet. Entering one raises `ValidationError` listing the `from_*` methods and the two-argument form,