pyvisa-rs 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyvisa_rs-0.1.1/.cargo/config.toml +2 -0
- pyvisa_rs-0.1.1/.claude/skills/ensure-parity/SKILL.md +243 -0
- pyvisa_rs-0.1.1/.github/.release-please-manifest.json +3 -0
- pyvisa_rs-0.1.1/.github/release-please-config.json +28 -0
- pyvisa_rs-0.1.1/.github/workflows/ci.yml +100 -0
- pyvisa_rs-0.1.1/.github/workflows/lint-pr-title.yml +38 -0
- pyvisa_rs-0.1.1/.github/workflows/release.yml +109 -0
- pyvisa_rs-0.1.1/.gitignore +2 -0
- pyvisa_rs-0.1.1/CHANGELOG.md +8 -0
- pyvisa_rs-0.1.1/CLAUDE.md +85 -0
- pyvisa_rs-0.1.1/Cargo.lock +637 -0
- pyvisa_rs-0.1.1/Cargo.toml +24 -0
- pyvisa_rs-0.1.1/PKG-INFO +69 -0
- pyvisa_rs-0.1.1/README.md +40 -0
- pyvisa_rs-0.1.1/docs/testing.md +58 -0
- pyvisa_rs-0.1.1/pyproject.toml +19 -0
- pyvisa_rs-0.1.1/python/pyvisa_rs/__init__.py +158 -0
- pyvisa_rs-0.1.1/pyvisa-py/.github/FUNDING.yml +12 -0
- pyvisa_rs-0.1.1/pyvisa-py/.github/ISSUE_TEMPLATE/bug_report.md +21 -0
- pyvisa_rs-0.1.1/pyvisa-py/.github/ISSUE_TEMPLATE/instrument-communication-issue.md +24 -0
- pyvisa_rs-0.1.1/pyvisa-py/.github/PULL_REQUEST_TEMPLATE.md +34 -0
- pyvisa_rs-0.1.1/pyvisa-py/.github/dependabot.yml +7 -0
- pyvisa_rs-0.1.1/pyvisa-py/.github/workflows/ci.yml +91 -0
- pyvisa_rs-0.1.1/pyvisa-py/.github/workflows/docs.yml +40 -0
- pyvisa_rs-0.1.1/pyvisa-py/.github/workflows/release.yml +125 -0
- pyvisa_rs-0.1.1/pyvisa-py/.gitignore +24 -0
- pyvisa_rs-0.1.1/pyvisa-py/.pre-commit-config.yaml +14 -0
- pyvisa_rs-0.1.1/pyvisa-py/.readthedocs.yaml +27 -0
- pyvisa_rs-0.1.1/pyvisa-py/AUTHORS +19 -0
- pyvisa_rs-0.1.1/pyvisa-py/CHANGES +234 -0
- pyvisa_rs-0.1.1/pyvisa-py/LICENSE +21 -0
- pyvisa_rs-0.1.1/pyvisa-py/MANIFEST.in +5 -0
- pyvisa_rs-0.1.1/pyvisa-py/README.rst +91 -0
- pyvisa_rs-0.1.1/pyvisa-py/azure-pipelines.yml +67 -0
- pyvisa_rs-0.1.1/pyvisa-py/bors.toml +3 -0
- pyvisa_rs-0.1.1/pyvisa-py/dev-requirements.txt +4 -0
- pyvisa_rs-0.1.1/pyvisa-py/docs/Makefile +153 -0
- pyvisa_rs-0.1.1/pyvisa-py/docs/index.rst +134 -0
- pyvisa_rs-0.1.1/pyvisa-py/docs/make.bat +190 -0
- pyvisa_rs-0.1.1/pyvisa-py/docs/requirements.txt +2 -0
- pyvisa_rs-0.1.1/pyvisa-py/docs/source/_static/logo-full.jpg +0 -0
- pyvisa_rs-0.1.1/pyvisa-py/docs/source/conf.py +305 -0
- pyvisa_rs-0.1.1/pyvisa-py/docs/source/faq.rst +122 -0
- pyvisa_rs-0.1.1/pyvisa-py/docs/source/index.rst +60 -0
- pyvisa_rs-0.1.1/pyvisa-py/docs/source/installation.rst +127 -0
- pyvisa_rs-0.1.1/pyvisa-py/pyproject.toml +123 -0
- pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/__init__.py +24 -0
- pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/attributes.py +31 -0
- pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/common.py +105 -0
- pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/gpib.py +1078 -0
- pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/highlevel.py +813 -0
- pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/prologix.py +437 -0
- pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/protocols/__init__.py +8 -0
- pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/protocols/hislip.py +787 -0
- pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/protocols/rpc.py +1062 -0
- pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/protocols/usbraw.py +81 -0
- pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/protocols/usbtmc.py +531 -0
- pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/protocols/usbutil.py +282 -0
- pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/protocols/vxi11.py +364 -0
- pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/protocols/xdrlib.py +249 -0
- pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/serial.py +462 -0
- pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/sessions.py +876 -0
- pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/tcpip.py +1475 -0
- pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/testsuite/__init__.py +25 -0
- pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/testsuite/keysight_assisted_tests/__init__.py +15 -0
- pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/testsuite/keysight_assisted_tests/test_resource_manager.py +45 -0
- pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/testsuite/keysight_assisted_tests/test_tcpip_resources.py +132 -0
- pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/testsuite/test_common.py +102 -0
- pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/testsuite/test_highlevel.py +25 -0
- pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/testsuite/test_serial.py +45 -0
- pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/testsuite/test_sessions.py +86 -0
- pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/usb.py +405 -0
- pyvisa_rs-0.1.1/pyvisa-rs/.gitignore +1 -0
- pyvisa_rs-0.1.1/pyvisa-rs/Cargo.toml +14 -0
- pyvisa_rs-0.1.1/pyvisa-rs/README.md +58 -0
- pyvisa_rs-0.1.1/pyvisa-rs/examples/discovery.py +102 -0
- pyvisa_rs-0.1.1/pyvisa-rs/src/lib.rs +369 -0
- pyvisa_rs-0.1.1/pyvisa-rs/uv.lock +35 -0
- pyvisa_rs-0.1.1/src/common.rs +127 -0
- pyvisa_rs-0.1.1/src/constants.rs +170 -0
- pyvisa_rs-0.1.1/src/lib.rs +104 -0
- pyvisa_rs-0.1.1/src/resource.rs +497 -0
- pyvisa_rs-0.1.1/src/rpc.rs +307 -0
- pyvisa_rs-0.1.1/src/serial.rs +460 -0
- pyvisa_rs-0.1.1/src/session.rs +527 -0
- pyvisa_rs-0.1.1/src/tcpip.rs +614 -0
- pyvisa_rs-0.1.1/src/usb.rs +767 -0
- pyvisa_rs-0.1.1/src/vxi11.rs +294 -0
- pyvisa_rs-0.1.1/tests/pyvisa_py_parity.rs +177 -0
- pyvisa_rs-0.1.1/tests/tcpip_socket.rs +62 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ensure-parity
|
|
3
|
+
description: Resolve a behavioural divergence between pyvisa-py and pyvisa-rs (or the rsvisa-rs crate). Use this whenever the user reports that the Rust port behaves differently from the vendored pyvisa-py reference — different discovery output, different read/write bytes, different error codes, different parsing, etc. The skill walks through reproducing the divergence, root-causing it inside `pyvisa-py/`, mirroring the upstream semantics in Rust, and adding regression coverage.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Ensure pyvisa-py ↔ rsvisa-rs Parity
|
|
7
|
+
|
|
8
|
+
The single load-bearing rule of this repo lives in `CLAUDE.md`:
|
|
9
|
+
|
|
10
|
+
> **rsvisa-rs aims for 100% behavioural parity with pyvisa-py.** When you find
|
|
11
|
+
> a divergence, the fix is to make rsvisa-rs match pyvisa-py — never to
|
|
12
|
+
> special-case the reported symptom.
|
|
13
|
+
|
|
14
|
+
This skill operationalises that rule. The user gives you code or a description
|
|
15
|
+
showing pyvisa-py and pyvisa-rs disagreeing; you produce a repro, find the
|
|
16
|
+
*root* upstream semantic, port it, and pin it down with a regression test.
|
|
17
|
+
|
|
18
|
+
## Inputs to expect
|
|
19
|
+
|
|
20
|
+
The user may hand over any of:
|
|
21
|
+
|
|
22
|
+
- A Python snippet using `pyvisa.ResourceManager("@py")` whose output differs
|
|
23
|
+
from the same snippet using `"@rs"`.
|
|
24
|
+
- A Rust snippet calling `rsvisa_rs::ResourceManager` directly.
|
|
25
|
+
- A discovery query (`cargo xtask visa-discovery <QUERY>`) returning different
|
|
26
|
+
resource sets between the two backends.
|
|
27
|
+
- A bug description ("`*IDN?` returns extra bytes on `@rs`", "`USB0::…` parses
|
|
28
|
+
but fails to open", "discovery is missing my Prologix interface", …).
|
|
29
|
+
|
|
30
|
+
If the user description is vague, ask **once** for the smallest snippet that
|
|
31
|
+
reproduces the divergence, then proceed — do not stall.
|
|
32
|
+
|
|
33
|
+
## The five stages
|
|
34
|
+
|
|
35
|
+
Work through these stages in order. Track them with the task tool so progress
|
|
36
|
+
is visible.
|
|
37
|
+
|
|
38
|
+
### 1. Reproduce the divergence
|
|
39
|
+
|
|
40
|
+
Goal: a deterministic, hardware-free repro that produces *different* outputs
|
|
41
|
+
on the two backends. Prefer in this order:
|
|
42
|
+
|
|
43
|
+
1. **A failing Rust unit test** in the relevant module (e.g.
|
|
44
|
+
`src/resource.rs`, `src/serial.rs`, `src/usb.rs`, `src/tcpip.rs`,
|
|
45
|
+
`src/vxi11.rs`). Use the existing `#[cfg(test)] mod tests` patterns and
|
|
46
|
+
pure inputs (strings, byte slices) wherever possible.
|
|
47
|
+
2. **The discovery xtask**, when the divergence is about which resources
|
|
48
|
+
appear from `list_resources`:
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
cargo xtask visa-discovery "<QUERY>"
|
|
52
|
+
cargo xtask visa-discovery --check "<QUERY>" # non-zero on mismatch
|
|
53
|
+
```
|
|
54
|
+
3. **The side-by-side Python script**, when the divergence is visible only
|
|
55
|
+
through PyVISA's higher-level API (timeouts, termchars, status codes
|
|
56
|
+
returned to Python):
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
cd pyvisa-rs && uv run \
|
|
60
|
+
--with pyvisa --with pyserial --with pyusb \
|
|
61
|
+
--with libusb-package --with zeroconf --with pyvisa-py \
|
|
62
|
+
--reinstall-package pyvisa-rs \
|
|
63
|
+
examples/discovery.py "<QUERY>"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Or, for arbitrary I/O experiments, write a one-off script that opens the
|
|
67
|
+
resource through both `@py` and `@rs`, performs the same calls, and prints
|
|
68
|
+
both transcripts. Run with the same `uv run --with …` set as above.
|
|
69
|
+
4. **A `tests/pyvisa_py_parity.rs`-style integration test** when hardware or
|
|
70
|
+
live Python is genuinely required. These are `#[ignore]`d by default —
|
|
71
|
+
note in the test which env var the user has to set to opt in.
|
|
72
|
+
|
|
73
|
+
Capture the *exact* observed difference (bytes, status codes, lists of
|
|
74
|
+
strings) before moving on. If you only have a human description and cannot
|
|
75
|
+
reproduce, say so explicitly and ask for more detail — do not invent a fix.
|
|
76
|
+
|
|
77
|
+
### 2. Root-cause inside pyvisa-py
|
|
78
|
+
|
|
79
|
+
This is the most important stage. The repo vendors pyvisa-py at
|
|
80
|
+
`pyvisa-py/pyvisa_py/` *as the source of truth*. Module names line up:
|
|
81
|
+
|
|
82
|
+
| Concern | pyvisa-py | rsvisa-rs |
|
|
83
|
+
|--------------------------------|------------------------------------------|-----------------|
|
|
84
|
+
| Resource name parsing/filter | `pyvisa-py/pyvisa_py/highlevel.py`, plus upstream `pyvisa.rname` (read from the installed `pyvisa` package via `python -c "import pyvisa.rname, inspect; print(inspect.getsourcefile(pyvisa.rname))"`) | `src/resource.rs` |
|
|
85
|
+
| Session dispatcher | `pyvisa_py/highlevel.py`, `pyvisa_py/sessions.py` | `src/session.rs` |
|
|
86
|
+
| Serial / ASRL | `pyvisa_py/serial.py` | `src/serial.rs` |
|
|
87
|
+
| TCPIP SOCKET / INSTR | `pyvisa_py/tcpip.py` | `src/tcpip.rs` |
|
|
88
|
+
| USB INSTR / RAW | `pyvisa_py/usb.py`, `pyvisa_py/protocols/usbtmc.py`, `usbraw.py`, `usbutil.py` | `src/usb.rs` |
|
|
89
|
+
| VXI-11 RPC | `pyvisa_py/protocols/vxi11.py`, `rpc.py`, `xdrlib.py` | `src/vxi11.rs`, `src/rpc.rs` |
|
|
90
|
+
| Constants / attribute IDs | upstream `pyvisa.constants` | `src/constants.rs` |
|
|
91
|
+
| Common helpers | `pyvisa_py/common.py` | `src/common.rs` |
|
|
92
|
+
|
|
93
|
+
Workflow:
|
|
94
|
+
|
|
95
|
+
1. **Open the corresponding pyvisa-py file** for the divergence area. Read
|
|
96
|
+
the full function or class, not just the line that seems relevant. Note
|
|
97
|
+
the *general* rule, not the specific case the user hit (e.g. "matches via
|
|
98
|
+
`re.match` with `?` → `.` substitution", not "this one resource string
|
|
99
|
+
isn't matched").
|
|
100
|
+
2. **Trace dependencies into upstream pyvisa** if pyvisa-py delegates to it
|
|
101
|
+
(e.g. `pyvisa.rname.filter`, `pyvisa.constants.StatusCode`,
|
|
102
|
+
`pyvisa.errors`). The vendored tree only contains pyvisa-py itself, so
|
|
103
|
+
look at the installed pyvisa via Python introspection:
|
|
104
|
+
|
|
105
|
+
```sh
|
|
106
|
+
uv run --with pyvisa python -c \
|
|
107
|
+
'import pyvisa.rname, inspect; print(inspect.getsourcefile(pyvisa.rname)); print(inspect.getsource(pyvisa.rname.filter))'
|
|
108
|
+
```
|
|
109
|
+
3. **State the upstream semantic out loud** before you write any Rust. A
|
|
110
|
+
one-paragraph "pyvisa-py does X because Y" note makes it obvious whether
|
|
111
|
+
the planned Rust change actually matches — and is worth saving as a code
|
|
112
|
+
comment if the rule is non-obvious (see the comment in
|
|
113
|
+
`src/resource.rs::filter` for the shape).
|
|
114
|
+
4. **Watch for upstream bugs / inconsistencies.** Per `CLAUDE.md`: when
|
|
115
|
+
pyvisa-py is itself wrong, *match it anyway* and flag the divergence to
|
|
116
|
+
the user. Don't silently improve on the reference; that breaks parity in
|
|
117
|
+
the other direction.
|
|
118
|
+
|
|
119
|
+
### 3. Implement the Rust fix
|
|
120
|
+
|
|
121
|
+
- Edit the matching file from the table above. Keep method names in
|
|
122
|
+
snake_case mirrors of the Python names (`open_default_resource_manager`,
|
|
123
|
+
`list_resources`, `read_stb`, `gpib_command`, …) — don't rename them.
|
|
124
|
+
- No backwards-compat shims, no speculative abstractions. If the new
|
|
125
|
+
semantic obsoletes existing code, delete the old code.
|
|
126
|
+
- Only add a code comment when the WHY is non-obvious — typically a pointer
|
|
127
|
+
to the pyvisa-py quirk being mirrored. The existing
|
|
128
|
+
`src/resource.rs::filter` comment is the template:
|
|
129
|
+
|
|
130
|
+
> Mirrors pyvisa.rname.filter: VISA queries are compiled as a
|
|
131
|
+
> case-insensitive regex (with `?` rewritten to `.`) and matched against
|
|
132
|
+
> each resource string with prefix-anchored semantics — i.e. Python's
|
|
133
|
+
> `re.match`, not whole-string `re.fullmatch`.
|
|
134
|
+
|
|
135
|
+
- After editing, run `cargo cleanup` (per the global instructions) to
|
|
136
|
+
auto-fix and format.
|
|
137
|
+
|
|
138
|
+
### 4. Lock in regression tests
|
|
139
|
+
|
|
140
|
+
Tests should encode the *general* upstream semantic, not just the failing
|
|
141
|
+
case. The parity rule is what's being protected.
|
|
142
|
+
|
|
143
|
+
- **Unit tests** in the same `#[cfg(test)] mod tests` block as the changed
|
|
144
|
+
code. Cover the rule and its corners (case-insensitivity, empty input,
|
|
145
|
+
alternation, anchoring, etc.) — see `src/resource.rs` `filter` tests for
|
|
146
|
+
the right granularity.
|
|
147
|
+
- **Discovery diff** is automatic via `cargo xtask visa-discovery --check`,
|
|
148
|
+
but if the change affects discovery, run it and confirm both backends
|
|
149
|
+
agree.
|
|
150
|
+
- **Integration test** in `tests/pyvisa_py_parity.rs` *only* when the rule
|
|
151
|
+
can only be observed through a live Python session or real hardware.
|
|
152
|
+
Mark with `#[ignore = "…"]` and document the env var in `docs/testing.md`
|
|
153
|
+
if a new one is introduced.
|
|
154
|
+
|
|
155
|
+
Before declaring done, run:
|
|
156
|
+
|
|
157
|
+
```sh
|
|
158
|
+
cargo cleanup
|
|
159
|
+
cargo test
|
|
160
|
+
cargo xtask visa-discovery --check # only if discovery may be affected
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
For divergences only visible through the PyO3 binding, also run the
|
|
164
|
+
discovery script with `--reinstall-package pyvisa-rs` to ensure uv rebuilds
|
|
165
|
+
the wheel against your Rust changes (uv's wheel cache does not invalidate on
|
|
166
|
+
Rust source edits — flag this if `@rs` results look stale).
|
|
167
|
+
|
|
168
|
+
### 5. Verify full parity
|
|
169
|
+
|
|
170
|
+
Stage 4 covers the *specific* rule with regression tests. This stage proves
|
|
171
|
+
that the originally-reported divergence — and any neighbouring behaviour —
|
|
172
|
+
actually agrees end-to-end between `@py` and `@rs`. Don't skip it: a fix
|
|
173
|
+
that passes the new unit tests but still shows different output in the
|
|
174
|
+
user's repro is not done.
|
|
175
|
+
|
|
176
|
+
Run both backends against the same inputs and compare:
|
|
177
|
+
|
|
178
|
+
1. **Re-run the stage-1 repro on the fixed code.** Whatever produced the
|
|
179
|
+
original divergence (xtask, side-by-side script, Python snippet, Rust
|
|
180
|
+
test) must now show identical output. If the repro was a Python snippet,
|
|
181
|
+
run it with both `pyvisa.ResourceManager("@py")` and `"@rs"` in the same
|
|
182
|
+
process and diff the results.
|
|
183
|
+
2. **Widen the comparison.** Run at least one of the broader cross-backend
|
|
184
|
+
checks so you catch collateral damage:
|
|
185
|
+
|
|
186
|
+
```sh
|
|
187
|
+
cargo xtask visa-discovery --check # discovery sets must match
|
|
188
|
+
cargo xtask visa-discovery --check "<QUERY>" # plus the query from the bug
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
And, for I/O-shaped divergences, the PyVISA-level side-by-side:
|
|
192
|
+
|
|
193
|
+
```sh
|
|
194
|
+
cd pyvisa-rs && uv run \
|
|
195
|
+
--with pyvisa --with pyserial --with pyusb \
|
|
196
|
+
--with libusb-package --with zeroconf --with pyvisa-py \
|
|
197
|
+
--reinstall-package pyvisa-rs \
|
|
198
|
+
examples/discovery.py --check
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
The `--reinstall-package pyvisa-rs` flag is mandatory after Rust edits;
|
|
202
|
+
without it uv serves a stale cached wheel and `@rs` will appear
|
|
203
|
+
unchanged.
|
|
204
|
+
3. **If hardware is in scope**, run the relevant `tests/pyvisa_py_parity.rs`
|
|
205
|
+
case with `RSVISA_PARITY_RESOURCE` / `RSVISA_SERIAL_RESOURCE` set:
|
|
206
|
+
|
|
207
|
+
```sh
|
|
208
|
+
RSVISA_PYTHON="uv run --with pyvisa --with pyserial --with pyusb \
|
|
209
|
+
--with libusb-package --with zeroconf python" \
|
|
210
|
+
cargo test --test pyvisa_py_parity -- --ignored
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
4. **Iterate.** If any of the above still shows divergence — even a
|
|
214
|
+
different one from what the user reported — go back to stage 2, find the
|
|
215
|
+
*new* upstream rule that's not being mirrored, port it, add a test, and
|
|
216
|
+
come back here. Repeat until every cross-backend check produces
|
|
217
|
+
byte-identical output (modulo intentionally-flagged pyvisa-py bugs).
|
|
218
|
+
5. **Only when everything matches, report back.** Keep it short (1–2
|
|
219
|
+
sentences):
|
|
220
|
+
- The upstream pyvisa-py rule(s) you mirrored, with file:line refs.
|
|
221
|
+
- Files changed in rsvisa-rs and which tests now cover them.
|
|
222
|
+
- Output from the verification commands you ran ("xtask discovery: no
|
|
223
|
+
diff; example/discovery.py: no diff").
|
|
224
|
+
- Any upstream bug you intentionally matched and want the user to know
|
|
225
|
+
about.
|
|
226
|
+
|
|
227
|
+
## Anti-patterns to avoid
|
|
228
|
+
|
|
229
|
+
- **Special-casing the reported symptom.** If a single resource string used
|
|
230
|
+
to be misfiltered, don't add an exception for that string — fix the rule.
|
|
231
|
+
- **Inventing a "better" behaviour.** Even if pyvisa-py's choice is weird,
|
|
232
|
+
parity is the goal. Surface the weirdness to the user as a note; don't
|
|
233
|
+
silently deviate.
|
|
234
|
+
- **Skipping the pyvisa-py read.** "I think it probably does X" is not a
|
|
235
|
+
substitute for opening the file. The repo vendors the source for a
|
|
236
|
+
reason.
|
|
237
|
+
- **Reading only the failing path.** Read the whole function so the test
|
|
238
|
+
you write covers the semantic, not the example.
|
|
239
|
+
- **Forgetting `--reinstall-package pyvisa-rs`** when iterating through the
|
|
240
|
+
PyO3 bindings via `uv run`. Stale wheels are the most common false
|
|
241
|
+
negative in this repo.
|
|
242
|
+
- **Renaming public API.** Snake-case mirrors of pyvisa-py's Python names
|
|
243
|
+
are deliberate; matching them is part of parity.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
|
|
3
|
+
"release-type": "simple",
|
|
4
|
+
"include-component-in-tag": false,
|
|
5
|
+
"include-v-in-tag": true,
|
|
6
|
+
"bump-minor-pre-major": true,
|
|
7
|
+
"bump-patch-for-minor-pre-major": true,
|
|
8
|
+
"draft": false,
|
|
9
|
+
"prerelease": false,
|
|
10
|
+
"packages": {
|
|
11
|
+
".": {
|
|
12
|
+
"package-name": "pyvisa-rs",
|
|
13
|
+
"changelog-path": "CHANGELOG.md",
|
|
14
|
+
"extra-files": [
|
|
15
|
+
{
|
|
16
|
+
"type": "toml",
|
|
17
|
+
"path": "pyvisa-rs/pyproject.toml",
|
|
18
|
+
"jsonpath": "$.project.version"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"type": "toml",
|
|
22
|
+
"path": "pyvisa-rs/Cargo.toml",
|
|
23
|
+
"jsonpath": "$.package.version"
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
concurrency:
|
|
9
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
10
|
+
cancel-in-progress: true
|
|
11
|
+
|
|
12
|
+
env:
|
|
13
|
+
CARGO_TERM_COLOR: always
|
|
14
|
+
RUST_BACKTRACE: 1
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
rust:
|
|
18
|
+
name: Rust (${{ matrix.os }})
|
|
19
|
+
runs-on: ${{ matrix.os }}
|
|
20
|
+
strategy:
|
|
21
|
+
fail-fast: false
|
|
22
|
+
matrix:
|
|
23
|
+
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v4
|
|
26
|
+
|
|
27
|
+
- name: Install Linux build deps
|
|
28
|
+
if: runner.os == 'Linux'
|
|
29
|
+
run: |
|
|
30
|
+
sudo apt-get update
|
|
31
|
+
sudo apt-get install -y libudev-dev libusb-1.0-0-dev
|
|
32
|
+
|
|
33
|
+
# PyO3's build script probes for a Python interpreter even when only
|
|
34
|
+
# checking/compiling the cdylib, so we set one up explicitly.
|
|
35
|
+
- uses: actions/setup-python@v5
|
|
36
|
+
with:
|
|
37
|
+
python-version: "3.12"
|
|
38
|
+
|
|
39
|
+
- name: Install stable toolchain (clippy)
|
|
40
|
+
uses: dtolnay/rust-toolchain@stable
|
|
41
|
+
with:
|
|
42
|
+
components: clippy
|
|
43
|
+
|
|
44
|
+
- uses: Swatinem/rust-cache@v2
|
|
45
|
+
|
|
46
|
+
- name: cargo fmt
|
|
47
|
+
run: cargo fmt --all -- --check
|
|
48
|
+
|
|
49
|
+
- name: cargo clippy
|
|
50
|
+
run: cargo clippy --workspace --all-targets -- -D warnings
|
|
51
|
+
|
|
52
|
+
- name: cargo test
|
|
53
|
+
run: cargo test --workspace
|
|
54
|
+
|
|
55
|
+
wheels:
|
|
56
|
+
name: Wheel (${{ matrix.os }})
|
|
57
|
+
runs-on: ${{ matrix.os }}
|
|
58
|
+
strategy:
|
|
59
|
+
fail-fast: false
|
|
60
|
+
matrix:
|
|
61
|
+
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
62
|
+
steps:
|
|
63
|
+
- uses: actions/checkout@v4
|
|
64
|
+
|
|
65
|
+
- uses: actions/setup-python@v5
|
|
66
|
+
with:
|
|
67
|
+
python-version: "3.12"
|
|
68
|
+
|
|
69
|
+
- name: Build wheel
|
|
70
|
+
uses: PyO3/maturin-action@v1
|
|
71
|
+
with:
|
|
72
|
+
working-directory: pyvisa-rs
|
|
73
|
+
command: build
|
|
74
|
+
args: --release --out dist
|
|
75
|
+
# auto picks the right manylinux container on Linux and is a no-op
|
|
76
|
+
# on macOS. The Linux container is RHEL-based so we install libusb /
|
|
77
|
+
# libudev via yum below.
|
|
78
|
+
manylinux: auto
|
|
79
|
+
before-script-linux: |
|
|
80
|
+
yum install -y libusbx-devel systemd-devel || \
|
|
81
|
+
(apt-get update && apt-get install -y libusb-1.0-0-dev libudev-dev)
|
|
82
|
+
|
|
83
|
+
- name: Smoke import
|
|
84
|
+
shell: bash
|
|
85
|
+
run: |
|
|
86
|
+
python -m pip install --upgrade pip
|
|
87
|
+
python -m pip install pyvisa
|
|
88
|
+
python -m pip install --no-index --find-links pyvisa-rs/dist pyvisa-rs
|
|
89
|
+
python - <<'PY'
|
|
90
|
+
import pyvisa
|
|
91
|
+
rm = pyvisa.ResourceManager("@rs")
|
|
92
|
+
print("debug info:", rm.visalib.get_debug_info())
|
|
93
|
+
PY
|
|
94
|
+
|
|
95
|
+
- name: Upload wheel
|
|
96
|
+
uses: actions/upload-artifact@v4
|
|
97
|
+
with:
|
|
98
|
+
name: wheel-${{ matrix.os }}
|
|
99
|
+
path: pyvisa-rs/dist/*.whl
|
|
100
|
+
if-no-files-found: error
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: lint pr title
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
branches: ["main"]
|
|
6
|
+
# Separate workflow so we can re-run when the title changes.
|
|
7
|
+
types: ["opened", "edited", "synchronize", "reopened"]
|
|
8
|
+
|
|
9
|
+
concurrency:
|
|
10
|
+
group: "${{ github.workflow }}-${{ github.event.pull_request.number }}"
|
|
11
|
+
cancel-in-progress: true
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
lint-title:
|
|
15
|
+
name: conventional commit pr title
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
steps:
|
|
18
|
+
- name: Install commitlint
|
|
19
|
+
# This release broke the default config https://github.com/conventional-changelog/commitlint/releases/tag/v18.6.1
|
|
20
|
+
# After they fix the config-conventional package we can upgrade.
|
|
21
|
+
run: npm install -g @commitlint/cli@18 @commitlint/config-conventional@18.6.0
|
|
22
|
+
- name: Lint PR name
|
|
23
|
+
env:
|
|
24
|
+
TITLE: ${{ github.event.pull_request.title }}
|
|
25
|
+
CONFIG_FILE: ${{ runner.temp }}/commitlint.config.js
|
|
26
|
+
CONFIG: |
|
|
27
|
+
module.exports = { extends: ["@commitlint/config-conventional"] };
|
|
28
|
+
run: |
|
|
29
|
+
echo "$CONFIG" > "$CONFIG_FILE"
|
|
30
|
+
echo "$TITLE" | commitlint --config "$CONFIG_FILE" > >(tee "$RUNNER_TEMP/lint-output.txt")
|
|
31
|
+
- name: Display helpful error message
|
|
32
|
+
if: failure()
|
|
33
|
+
env:
|
|
34
|
+
TITLE: ${{ github.event.pull_request.title }}
|
|
35
|
+
run: |
|
|
36
|
+
echo "🚨 Your PR Title: \"${TITLE}\" did not pass linting" >> $GITHUB_STEP_SUMMARY
|
|
37
|
+
cat "$RUNNER_TEMP/lint-output.txt" >> $GITHUB_STEP_SUMMARY
|
|
38
|
+
echo "Please update it to follow conventional commit format: https://www.conventionalcommits.org/" >> $GITHUB_STEP_SUMMARY
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: write
|
|
9
|
+
pull-requests: write
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
release-please:
|
|
13
|
+
name: release-please
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
outputs:
|
|
16
|
+
release_created: ${{ steps.release.outputs.release_created }}
|
|
17
|
+
tag_name: ${{ steps.release.outputs.tag_name }}
|
|
18
|
+
steps:
|
|
19
|
+
- uses: googleapis/release-please-action@v4
|
|
20
|
+
id: release
|
|
21
|
+
with:
|
|
22
|
+
config-file: .github/release-please-config.json
|
|
23
|
+
manifest-file: .github/.release-please-manifest.json
|
|
24
|
+
|
|
25
|
+
wheels:
|
|
26
|
+
name: Wheel (${{ matrix.os }})
|
|
27
|
+
needs: release-please
|
|
28
|
+
if: needs.release-please.outputs.release_created == 'true'
|
|
29
|
+
runs-on: ${{ matrix.os }}
|
|
30
|
+
strategy:
|
|
31
|
+
fail-fast: false
|
|
32
|
+
matrix:
|
|
33
|
+
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
34
|
+
steps:
|
|
35
|
+
- uses: actions/checkout@v4
|
|
36
|
+
with:
|
|
37
|
+
ref: ${{ needs.release-please.outputs.tag_name }}
|
|
38
|
+
|
|
39
|
+
- uses: actions/setup-python@v5
|
|
40
|
+
with:
|
|
41
|
+
python-version: "3.12"
|
|
42
|
+
|
|
43
|
+
- name: Build wheel
|
|
44
|
+
uses: PyO3/maturin-action@v1
|
|
45
|
+
with:
|
|
46
|
+
working-directory: pyvisa-rs
|
|
47
|
+
command: build
|
|
48
|
+
args: --release --out dist
|
|
49
|
+
manylinux: auto
|
|
50
|
+
before-script-linux: |
|
|
51
|
+
yum install -y libusbx-devel systemd-devel || \
|
|
52
|
+
(apt-get update && apt-get install -y libusb-1.0-0-dev libudev-dev)
|
|
53
|
+
|
|
54
|
+
- name: Upload wheel
|
|
55
|
+
uses: actions/upload-artifact@v4
|
|
56
|
+
with:
|
|
57
|
+
name: wheel-${{ matrix.os }}
|
|
58
|
+
path: pyvisa-rs/dist/*.whl
|
|
59
|
+
if-no-files-found: error
|
|
60
|
+
|
|
61
|
+
sdist:
|
|
62
|
+
name: sdist
|
|
63
|
+
needs: release-please
|
|
64
|
+
if: needs.release-please.outputs.release_created == 'true'
|
|
65
|
+
runs-on: ubuntu-latest
|
|
66
|
+
steps:
|
|
67
|
+
- uses: actions/checkout@v4
|
|
68
|
+
with:
|
|
69
|
+
ref: ${{ needs.release-please.outputs.tag_name }}
|
|
70
|
+
|
|
71
|
+
- uses: actions/setup-python@v5
|
|
72
|
+
with:
|
|
73
|
+
python-version: "3.12"
|
|
74
|
+
|
|
75
|
+
- name: Build sdist
|
|
76
|
+
uses: PyO3/maturin-action@v1
|
|
77
|
+
with:
|
|
78
|
+
working-directory: pyvisa-rs
|
|
79
|
+
command: sdist
|
|
80
|
+
args: --out dist
|
|
81
|
+
|
|
82
|
+
- name: Upload sdist
|
|
83
|
+
uses: actions/upload-artifact@v4
|
|
84
|
+
with:
|
|
85
|
+
name: sdist
|
|
86
|
+
path: pyvisa-rs/dist/*.tar.gz
|
|
87
|
+
if-no-files-found: error
|
|
88
|
+
|
|
89
|
+
publish:
|
|
90
|
+
name: Publish to PyPI
|
|
91
|
+
needs: [release-please, wheels, sdist]
|
|
92
|
+
if: needs.release-please.outputs.release_created == 'true'
|
|
93
|
+
runs-on: ubuntu-latest
|
|
94
|
+
environment:
|
|
95
|
+
name: pypi
|
|
96
|
+
url: https://pypi.org/project/pyvisa-rs/
|
|
97
|
+
permissions:
|
|
98
|
+
id-token: write
|
|
99
|
+
steps:
|
|
100
|
+
- name: Download wheels and sdist
|
|
101
|
+
uses: actions/download-artifact@v4
|
|
102
|
+
with:
|
|
103
|
+
path: dist
|
|
104
|
+
merge-multiple: true
|
|
105
|
+
|
|
106
|
+
- name: Publish to PyPI
|
|
107
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
108
|
+
with:
|
|
109
|
+
packages-dir: dist
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.1](https://github.com/nominal-io/rsvisa-rs/compare/v0.1.0...v0.1.1) (2026-05-20)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **ci:** add release-please + PyPI publish workflow ([#1](https://github.com/nominal-io/rsvisa-rs/issues/1)) ([0596b31](https://github.com/nominal-io/rsvisa-rs/commit/0596b31c179b464781e55c0420a441a7918bf893))
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# rsvisa-rs
|
|
2
|
+
|
|
3
|
+
A Rust port of pyvisa-py's pure-Python VISA backend, plus PyO3 bindings
|
|
4
|
+
(`pyvisa-rs`) that let PyVISA load the Rust implementation as the `@rs`
|
|
5
|
+
backend. The Rust crate mirrors pyvisa-py module-for-module so a user can
|
|
6
|
+
swap backends without behaviour changing.
|
|
7
|
+
|
|
8
|
+
## Layout
|
|
9
|
+
|
|
10
|
+
- `src/` — the `rsvisa-rs` crate. Module names track pyvisa-py:
|
|
11
|
+
`serial.rs` ↔ `pyvisa_py/serial.py`, `tcpip.rs` ↔ `tcpip.py`,
|
|
12
|
+
`usb.rs` ↔ `usb.py`, `vxi11.rs` ↔ `protocols/vxi11.py`, etc.
|
|
13
|
+
`session.rs` is the `ResourceManager` dispatcher (mirrors pyvisa-py's
|
|
14
|
+
`highlevel.PyVisaLibrary` + `sessions.Session`).
|
|
15
|
+
- `pyvisa-rs/` — PyO3 bindings. `src/lib.rs` exposes a `#[pyclass]`
|
|
16
|
+
wrapping `ResourceManager`; `python/pyvisa_rs/__init__.py` is the
|
|
17
|
+
`VisaLibraryBase` subclass that PyVISA discovers via `pyvisa_*` package
|
|
18
|
+
scanning. `examples/discovery.py` runs both backends side-by-side.
|
|
19
|
+
- `pyvisa-py/` — the **vendored upstream Python implementation**. This is
|
|
20
|
+
the source of truth for behaviour; consult it before adding new features
|
|
21
|
+
or fixing bugs.
|
|
22
|
+
- `xtask/` — `cargo xtask visa-discovery` runs discovery against both
|
|
23
|
+
backends and diffs the results. Used by `tests/pyvisa_py_parity.rs`.
|
|
24
|
+
- `tests/` — integration tests, including pyvisa-py parity.
|
|
25
|
+
|
|
26
|
+
## The parity rule
|
|
27
|
+
|
|
28
|
+
**rsvisa-rs aims for 100% behavioural parity with pyvisa-py.** When you
|
|
29
|
+
find a divergence, the fix is to make rsvisa-rs match pyvisa-py — never
|
|
30
|
+
to special-case the reported symptom.
|
|
31
|
+
|
|
32
|
+
Before writing a fix:
|
|
33
|
+
|
|
34
|
+
1. Open the corresponding file in `pyvisa-py/pyvisa_py/`.
|
|
35
|
+
2. Read the pyvisa-py implementation and understand its full semantics,
|
|
36
|
+
not just the case the user reported.
|
|
37
|
+
3. Replicate those semantics in Rust. If pyvisa-py uses a regex,
|
|
38
|
+
pyserial behaviour, or a libusb quirk, the Rust port should produce
|
|
39
|
+
the same observable result for **all** inputs, not just the failing
|
|
40
|
+
one.
|
|
41
|
+
4. Add a regression test that captures the broader semantic, not just
|
|
42
|
+
the specific bug.
|
|
43
|
+
|
|
44
|
+
When pyvisa-py itself is inconsistent or buggy, prefer matching it
|
|
45
|
+
anyway and flag the divergence to the user — don't silently "improve"
|
|
46
|
+
on the reference.
|
|
47
|
+
|
|
48
|
+
## Commands
|
|
49
|
+
|
|
50
|
+
- `cargo xtask visa-discovery [QUERY]` — diff discovery between
|
|
51
|
+
rsvisa-rs and the vendored pyvisa-py. `--check` exits non-zero on
|
|
52
|
+
divergence.
|
|
53
|
+
- From `pyvisa-rs/`:
|
|
54
|
+
`uv run --with pyvisa --with pyserial --with pyusb --with libusb-package --with zeroconf --with pyvisa-py examples/discovery.py [QUERY]`
|
|
55
|
+
— same diff, but through the PyO3 bindings (the `@rs` backend) vs
|
|
56
|
+
pyvisa-py both running inside PyVISA. Pass `--reinstall-package pyvisa-rs`
|
|
57
|
+
after Rust source changes; uv's wheel cache does not invalidate on
|
|
58
|
+
Rust source edits.
|
|
59
|
+
- `cd pyvisa-rs && maturin develop` — fast editable iteration on the
|
|
60
|
+
PyO3 bindings inside an activated venv.
|
|
61
|
+
|
|
62
|
+
## Style
|
|
63
|
+
|
|
64
|
+
- No comments unless they explain a non-obvious WHY (a pyvisa-py quirk
|
|
65
|
+
being mirrored, an unintuitive VISA spec rule, a workaround for a
|
|
66
|
+
specific platform). Don't restate what the code does.
|
|
67
|
+
- Don't add backwards-compatibility shims or speculative abstractions.
|
|
68
|
+
- The Rust API uses snake_case names that match pyvisa-py's Python
|
|
69
|
+
method names (`open_default_resource_manager`, `list_resources`,
|
|
70
|
+
`read_stb`, `gpib_command`, …). Don't rename them.
|
|
71
|
+
|
|
72
|
+
## Releases
|
|
73
|
+
|
|
74
|
+
PR titles must follow [Conventional Commits](https://www.conventionalcommits.org/)
|
|
75
|
+
(`feat:`, `fix:`, `feat!:` for breaking, `chore:`, `docs:`, …) — this is
|
|
76
|
+
enforced by the `lint pr title` workflow and drives release-please's
|
|
77
|
+
semver bumps.
|
|
78
|
+
|
|
79
|
+
## Dependency notes
|
|
80
|
+
|
|
81
|
+
- `rusb` (libusb) needs `libusb-1.0-0-dev` on Linux. Windows is not
|
|
82
|
+
currently in CI because `libusb1-sys` doesn't vendor by default.
|
|
83
|
+
- `serialport` needs `libudev-dev` on Linux.
|
|
84
|
+
- The PyO3 binding is built with `abi3-py310`, so a single wheel covers
|
|
85
|
+
Python 3.10+.
|