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.
Files changed (90) hide show
  1. pyvisa_rs-0.1.1/.cargo/config.toml +2 -0
  2. pyvisa_rs-0.1.1/.claude/skills/ensure-parity/SKILL.md +243 -0
  3. pyvisa_rs-0.1.1/.github/.release-please-manifest.json +3 -0
  4. pyvisa_rs-0.1.1/.github/release-please-config.json +28 -0
  5. pyvisa_rs-0.1.1/.github/workflows/ci.yml +100 -0
  6. pyvisa_rs-0.1.1/.github/workflows/lint-pr-title.yml +38 -0
  7. pyvisa_rs-0.1.1/.github/workflows/release.yml +109 -0
  8. pyvisa_rs-0.1.1/.gitignore +2 -0
  9. pyvisa_rs-0.1.1/CHANGELOG.md +8 -0
  10. pyvisa_rs-0.1.1/CLAUDE.md +85 -0
  11. pyvisa_rs-0.1.1/Cargo.lock +637 -0
  12. pyvisa_rs-0.1.1/Cargo.toml +24 -0
  13. pyvisa_rs-0.1.1/PKG-INFO +69 -0
  14. pyvisa_rs-0.1.1/README.md +40 -0
  15. pyvisa_rs-0.1.1/docs/testing.md +58 -0
  16. pyvisa_rs-0.1.1/pyproject.toml +19 -0
  17. pyvisa_rs-0.1.1/python/pyvisa_rs/__init__.py +158 -0
  18. pyvisa_rs-0.1.1/pyvisa-py/.github/FUNDING.yml +12 -0
  19. pyvisa_rs-0.1.1/pyvisa-py/.github/ISSUE_TEMPLATE/bug_report.md +21 -0
  20. pyvisa_rs-0.1.1/pyvisa-py/.github/ISSUE_TEMPLATE/instrument-communication-issue.md +24 -0
  21. pyvisa_rs-0.1.1/pyvisa-py/.github/PULL_REQUEST_TEMPLATE.md +34 -0
  22. pyvisa_rs-0.1.1/pyvisa-py/.github/dependabot.yml +7 -0
  23. pyvisa_rs-0.1.1/pyvisa-py/.github/workflows/ci.yml +91 -0
  24. pyvisa_rs-0.1.1/pyvisa-py/.github/workflows/docs.yml +40 -0
  25. pyvisa_rs-0.1.1/pyvisa-py/.github/workflows/release.yml +125 -0
  26. pyvisa_rs-0.1.1/pyvisa-py/.gitignore +24 -0
  27. pyvisa_rs-0.1.1/pyvisa-py/.pre-commit-config.yaml +14 -0
  28. pyvisa_rs-0.1.1/pyvisa-py/.readthedocs.yaml +27 -0
  29. pyvisa_rs-0.1.1/pyvisa-py/AUTHORS +19 -0
  30. pyvisa_rs-0.1.1/pyvisa-py/CHANGES +234 -0
  31. pyvisa_rs-0.1.1/pyvisa-py/LICENSE +21 -0
  32. pyvisa_rs-0.1.1/pyvisa-py/MANIFEST.in +5 -0
  33. pyvisa_rs-0.1.1/pyvisa-py/README.rst +91 -0
  34. pyvisa_rs-0.1.1/pyvisa-py/azure-pipelines.yml +67 -0
  35. pyvisa_rs-0.1.1/pyvisa-py/bors.toml +3 -0
  36. pyvisa_rs-0.1.1/pyvisa-py/dev-requirements.txt +4 -0
  37. pyvisa_rs-0.1.1/pyvisa-py/docs/Makefile +153 -0
  38. pyvisa_rs-0.1.1/pyvisa-py/docs/index.rst +134 -0
  39. pyvisa_rs-0.1.1/pyvisa-py/docs/make.bat +190 -0
  40. pyvisa_rs-0.1.1/pyvisa-py/docs/requirements.txt +2 -0
  41. pyvisa_rs-0.1.1/pyvisa-py/docs/source/_static/logo-full.jpg +0 -0
  42. pyvisa_rs-0.1.1/pyvisa-py/docs/source/conf.py +305 -0
  43. pyvisa_rs-0.1.1/pyvisa-py/docs/source/faq.rst +122 -0
  44. pyvisa_rs-0.1.1/pyvisa-py/docs/source/index.rst +60 -0
  45. pyvisa_rs-0.1.1/pyvisa-py/docs/source/installation.rst +127 -0
  46. pyvisa_rs-0.1.1/pyvisa-py/pyproject.toml +123 -0
  47. pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/__init__.py +24 -0
  48. pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/attributes.py +31 -0
  49. pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/common.py +105 -0
  50. pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/gpib.py +1078 -0
  51. pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/highlevel.py +813 -0
  52. pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/prologix.py +437 -0
  53. pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/protocols/__init__.py +8 -0
  54. pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/protocols/hislip.py +787 -0
  55. pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/protocols/rpc.py +1062 -0
  56. pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/protocols/usbraw.py +81 -0
  57. pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/protocols/usbtmc.py +531 -0
  58. pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/protocols/usbutil.py +282 -0
  59. pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/protocols/vxi11.py +364 -0
  60. pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/protocols/xdrlib.py +249 -0
  61. pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/serial.py +462 -0
  62. pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/sessions.py +876 -0
  63. pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/tcpip.py +1475 -0
  64. pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/testsuite/__init__.py +25 -0
  65. pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/testsuite/keysight_assisted_tests/__init__.py +15 -0
  66. pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/testsuite/keysight_assisted_tests/test_resource_manager.py +45 -0
  67. pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/testsuite/keysight_assisted_tests/test_tcpip_resources.py +132 -0
  68. pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/testsuite/test_common.py +102 -0
  69. pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/testsuite/test_highlevel.py +25 -0
  70. pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/testsuite/test_serial.py +45 -0
  71. pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/testsuite/test_sessions.py +86 -0
  72. pyvisa_rs-0.1.1/pyvisa-py/pyvisa_py/usb.py +405 -0
  73. pyvisa_rs-0.1.1/pyvisa-rs/.gitignore +1 -0
  74. pyvisa_rs-0.1.1/pyvisa-rs/Cargo.toml +14 -0
  75. pyvisa_rs-0.1.1/pyvisa-rs/README.md +58 -0
  76. pyvisa_rs-0.1.1/pyvisa-rs/examples/discovery.py +102 -0
  77. pyvisa_rs-0.1.1/pyvisa-rs/src/lib.rs +369 -0
  78. pyvisa_rs-0.1.1/pyvisa-rs/uv.lock +35 -0
  79. pyvisa_rs-0.1.1/src/common.rs +127 -0
  80. pyvisa_rs-0.1.1/src/constants.rs +170 -0
  81. pyvisa_rs-0.1.1/src/lib.rs +104 -0
  82. pyvisa_rs-0.1.1/src/resource.rs +497 -0
  83. pyvisa_rs-0.1.1/src/rpc.rs +307 -0
  84. pyvisa_rs-0.1.1/src/serial.rs +460 -0
  85. pyvisa_rs-0.1.1/src/session.rs +527 -0
  86. pyvisa_rs-0.1.1/src/tcpip.rs +614 -0
  87. pyvisa_rs-0.1.1/src/usb.rs +767 -0
  88. pyvisa_rs-0.1.1/src/vxi11.rs +294 -0
  89. pyvisa_rs-0.1.1/tests/pyvisa_py_parity.rs +177 -0
  90. pyvisa_rs-0.1.1/tests/tcpip_socket.rs +62 -0
@@ -0,0 +1,2 @@
1
+ [alias]
2
+ xtask = "run --package xtask --"
@@ -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,3 @@
1
+ {
2
+ ".": "0.1.1"
3
+ }
@@ -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,2 @@
1
+ /target
2
+ /.claude/
@@ -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+.